Skip to content

pm-pl/session-utils

 
 

Repository files navigation

session-utils

Poggit CI Stars License


Logo

session-utils

General-purpose player session management library

Korean README · Report a bug · Request a feature


Overview

session-utils is a virion for PMMP plugin developers that eliminates session management boilerplate. Instead of writing lifecycle listeners and event routing from scratch for every feature, you declare what you need and the library handles the rest.

What it does:

  • Automatically creates and destroys sessions on player join/quit (for lifecycle sessions)
  • Routes PMMP events to the correct player's session using #[SessionEventHandler] attributes — no listener classes to write
  • Supports ordered session progression via SessionSequence for multi-step flows like tutorials
  • Prevents duplicate PMMP listener registration across multiple session types via a global registry
  • Provides a generic, type-safe SessionManager<T> for clean plugin architecture

Requirements

  • PocketMine-MP 5.x
  • PHP 8.2+

Architecture

classDiagram
direction LR

class Session {
    <<abstract>>
    -Player player
    -SessionManager sessionManager
    -bool active
    +getPlayer() Player
    +isActive() bool
    +start() void
    +terminate(reason) void
    #onStart() void
    #onTerminate(reason) void
}

class SequenceSession {
    <<abstract>>
    #next(reason) void
}

class LifecycleSession {
    <<interface>>
}

class SessionManager~T extends Session~ {
    -array sessions
    -list eventBindingList
    +getSession(playerOrId) T?
    +getSessionOrThrow(playerOrId) T
    +getOrCreateSession(player) T?
    +createSession(player, ...args) T?
    +getAllSessions() list~T~
    +removeSession(session, reason) void
    +terminateAll(reason) int
    +getSessionClass() class-string
    +onSessionCreated(callback) self
    +onSessionTerminated(callback) self
}

class SequenceSessionManager {
    +setNext(manager) void
    +setOnExhausted(callback) void
    +progressNext(player, reason) void
}

class SessionSequence {
    +start(player) void
    +startFrom(player, step) void
    +onComplete(callback) self
    +terminateAll(reason) void
}

class SessionEventListenerRegistry {
    <<singleton>>
    -array listeners
    +attachBinding(binding, plugin) void
    +detachBinding(binding) void
}

class SessionEventListener {
    -array eventDispatchers
    -RegisteredListener registeredListener
    +onEvent(event) void
    +attachBinding(dispatcher) void
    +detachBinding(dispatcher) void
    +hasDispatchers() bool
}

class BaseSessionEventDispatcher {
    <<abstract>>
    +string eventKey
    +string eventClass
    +int priority
    +bool handleCancelled
    +string methodName
    +dispatch(event, player) void
}

class SessionMethodDispatcher {
    +dispatch(event, player) void
}

class ManagerMethodDispatcher {
    +dispatch(event, player) void
}

class SessionEventHandler {
    <<attribute>>
    +string eventClass
    +int priority
    +bool handleCancelled
}

Session <|-- SequenceSession
Session <|-- LifecycleSession
SessionManager <|-- SequenceSessionManager
SessionSequence --> SequenceSessionManager : creates and chains
SequenceSessionManager --> SequenceSession : manages
SessionManager --> Session : manages
SessionManager --> SessionEventListenerRegistry : registers via
SessionEventListenerRegistry --> SessionEventListener : owns one per eventKey
SessionEventListener --> BaseSessionEventDispatcher : dispatches to
BaseSessionEventDispatcher <|-- SessionMethodDispatcher
BaseSessionEventDispatcher <|-- ManagerMethodDispatcher
SessionMethodDispatcher ..> SessionEventHandler : created from
Loading

Event flow

PMMP fires event
  → SessionEventListener::onEvent()
    → [cancelled mid-dispatch? stop if handleCancelled=false]
    → BaseSessionEventDispatcher::dispatch()
      ├─ SessionMethodDispatcher → SessionManager::getSession(player) → Session::{methodName}(event)
      └─ ManagerMethodDispatcher → SessionManager::{methodName}(event)

Core Components

Session (abstract class)

Base class for all session types. Holds the Player and SessionManager references and manages active state. Exposes onStart() and onTerminate() hooks for subclasses to implement.

Calling terminate() inside a session class will self-terminate the session via the owning SessionManager.

SequenceSession (abstract class)

Extends Session for use within a SessionSequence. Adds next() to advance the sequence to the next step. Must not implement LifecycleSession.

Key methods:

  • protected function next(string $reason) — Terminates this session and starts the next step in the sequence. If this is the last step, the sequence's onComplete callback is invoked instead.

LifecycleSession (interface)

Marker interface. Classes implementing this are automatically created on PlayerJoinEvent and destroyed on PlayerQuitEvent by SessionManager. Must not be combined with SequenceSession.

SessionSequence (class)

Manages an ordered progression of SequenceSession subclasses for a single player. Internally creates and chains a SequenceSessionManager per step.

All session classes passed to SessionSequence must extend SequenceSession and must not implement LifecycleSession — this is enforced at construction time.

Key methods:

  • start(Player $player) — Starts the sequence from the first step.
  • startFrom(Player $player, int|string $step) — Starts from a specific step by 0-based index or class-string.
  • onComplete(Closure $callback) — Registers a callback invoked when the last session calls next().
  • terminateAll(string $reason) — Terminates all active sessions across all steps.

SessionManager<T> (class)

Central orchestrator for one session type. On construction:

  1. Collects lifecycle dispatchers for PlayerJoinEvent / PlayerQuitEvent (if LifecycleSession)
  2. Scans the session class for #[SessionEventHandler] attributes
  3. Registers all collected dispatchers with SessionEventListenerRegistry

Key methods:

  • getSession(Player|int) — Returns the active session or null.
  • getSessionOrThrow(Player|int) — Returns the active session or throws if none exists.
  • getOrCreateSession(Player) — Returns the active session or creates one if none exists.
  • onSessionCreated(Closure) — Registers a callback invoked after a session is created and started.
  • onSessionTerminated(Closure) — Registers a callback invoked after a session is terminated and removed.

#[SessionEventHandler] (attribute)

Declares a method as a session-scoped event handler. Can be applied multiple times on the same method for different events. The method must be public and accept exactly one non-nullable Event subclass parameter.

SessionEventListenerRegistry (singleton)

Ensures only one PMMP listener exists per unique (eventClass, priority, handleCancelled) combination — the **eventKey **. Multiple session types subscribing to the same event share a single PMMP listener. This applies to both session event handlers and lifecycle events (PlayerJoinEvent, PlayerQuitEvent).

SessionEventListener (class)

The actual PMMP-registered listener for one eventKey. Holds a list of BaseSessionEventDispatcher instances and routes each fired event to them in registration order. Respects cancellation state mid-dispatch.

BaseSessionEventDispatcher (abstract class)

Base class for all event dispatchers. Holds the listener configuration (eventKey, eventClass, priority, handleCancelled, methodName) and defines the dispatch() contract. Two concrete subclasses are provided:

  • SessionMethodDispatcher — Looks up the player's active session and invokes the bound method on it.
  • ManagerMethodDispatcher — Invokes the bound method directly on the SessionManager instance. Used internally for lifecycle event handling.

SessionTerminateReasons (interface)

Built-in termination reason constants. Custom string reasons are allowed — these exist to avoid typos and keep semantics consistent across plugins.


File Structure

src/kim/present/utils/session/
├── Session.php
├── SequenceSession.php
├── LifecycleSession.php
├── SessionManager.php
├── SequenceSessionManager.php
├── SessionSequence.php
├── SessionTerminateReasons.php
└── listener/
    ├── SessionEventListener.php
    ├── SessionEventListenerRegistry.php
    ├── attribute/
    │   └── SessionEventHandler.php
    └── dispatcher/
        ├── BaseSessionEventDispatcher.php
        ├── SessionMethodDispatcher.php
        └── ManagerMethodDispatcher.php

Usage

1. Define a lifecycle session

Extend Session and implement LifecycleSession for automatic join/quit management. Declare event handlers with #[SessionEventHandler] — no separate listener class needed.

use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\player\PlayerInteractEvent;
use kim\present\utils\session\Session;
use kim\present\utils\session\LifecycleSession;
use kim\present\utils\session\SessionTerminateReasons;
use kim\present\utils\session\listener\attribute\SessionEventHandler;

final class WorldEditSession extends Session implements LifecycleSession{

    private ?array $pos1 = null;
    private ?array $pos2 = null;

    protected function onStart() : void{
        $this->getPlayer()->sendMessage("WorldEdit session started.");
    }

    protected function onTerminate(string $reason) : void{
        // save state, clean up, etc.
    }

    #[SessionEventHandler(BlockBreakEvent::class)]
    public function onBlockBreak(BlockBreakEvent $event) : void{
        $pos = $event->getBlock()->getPosition();
        $this->pos1 = [$pos->x, $pos->y, $pos->z];
        $event->cancel();
    }

    #[SessionEventHandler(PlayerInteractEvent::class)]
    public function onInteract(PlayerInteractEvent $event) : void{
        $pos = $event->getBlock()->getPosition();
        $this->pos2 = [$pos->x, $pos->y, $pos->z];

        if($this->pos1 !== null){
            // Self-terminate when both positions are selected
            $this->terminate(SessionTerminateReasons::COMPLETED);
        }
    }
}

2. Bootstrap from your plugin

use pocketmine\plugin\PluginBase;
use kim\present\utils\session\SessionManager;
use kim\present\utils\session\SessionTerminateReasons;

final class MyPlugin extends PluginBase{
    private SessionManager $sessionManager;

    protected function onEnable() : void{
        $this->sessionManager = new SessionManager($this, WorldEditSession::class);

        $this->sessionManager
            ->onSessionCreated(function(WorldEditSession $session) : void{
                // e.g. log session start
            })
            ->onSessionTerminated(function(WorldEditSession $session, string $reason) : void{
                // e.g. persist state to DB
            });
    }

    protected function onDisable() : void{
        $this->sessionManager->terminateAll(SessionTerminateReasons::PLUGIN_DISABLE);
    }
}

3. Manage sessions manually

// Create a session on demand (for non-lifecycle sessions)
$session = $this->sessionManager->createSession($player);

// Retrieve a session (returns null if none exists)
$session = $this->sessionManager->getSession($player);

// Retrieve a session or throw if none exists
$session = $this->sessionManager->getSessionOrThrow($player);

// Retrieve a session or create one if none exists
$session = $this->sessionManager->getOrCreateSession($player);

// Remove a specific session
$this->sessionManager->removeSession($player, SessionTerminateReasons::MANUAL);

// Terminate all sessions (e.g. on plugin disable)
$count = $this->sessionManager->terminateAll(SessionTerminateReasons::PLUGIN_DISABLE);

Lifecycle sessions vs. task sessions

Lifecycle session Task session
Interface LifecycleSession (none)
Created Automatically on PlayerJoinEvent Manually via createSession()
Destroyed Automatically on PlayerQuitEvent Manually via removeSession()
Use case Per-player persistent state On-demand feature sessions

4. Define a sequence session

For multi-step flows (e.g. tutorials), extend SequenceSession and call next() to advance to the next step.

use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\block\BlockPlaceEvent;
use kim\present\utils\session\SequenceSession;
use kim\present\utils\session\listener\attribute\SessionEventHandler;

final class TutorialStep1Session extends SequenceSession{

    protected function onStart() : void{
        $this->getPlayer()->sendMessage("Step 1: Break a block.");
    }

    protected function onTerminate(string $reason) : void{}

    #[SessionEventHandler(BlockBreakEvent::class)]
    public function onBlockBreak(BlockBreakEvent $event) : void{
        $this->getPlayer()->sendMessage("Step 1 complete!");
        $this->next(); // Advances to TutorialStep2Session
    }
}

final class TutorialStep2Session extends SequenceSession{

    protected function onStart() : void{
        $this->getPlayer()->sendMessage("Step 2: Place a block.");
    }

    protected function onTerminate(string $reason) : void{}

    #[SessionEventHandler(BlockPlaceEvent::class)]
    public function onBlockPlace(BlockPlaceEvent $event) : void{
        $this->getPlayer()->sendMessage("Step 2 complete!");
        $this->next(); // No next step — triggers onComplete callback
    }
}

5. Bootstrap a sequence

use pocketmine\plugin\PluginBase;
use pocketmine\player\Player;
use kim\present\utils\session\SessionSequence;
use kim\present\utils\session\SessionTerminateReasons;

final class MyPlugin extends PluginBase{
    private SessionSequence $tutorialSequence;

    protected function onEnable() : void{
        $this->tutorialSequence = new SessionSequence($this,
            TutorialStep1Session::class,
            TutorialStep2Session::class,
        );

        $this->tutorialSequence->onComplete(function(Player $player) : void{
            $player->sendMessage("Tutorial complete!");
            $this->saveProgress($player, completed: true);
        });
    }

    protected function onDisable() : void{
        $this->tutorialSequence->terminateAll(SessionTerminateReasons::PLUGIN_DISABLE);
    }

    public function startTutorial(Player $player) : void{
        $progress = $this->loadProgress($player); // e.g. 0, 1

        if($progress === 0){
            $this->tutorialSequence->start($player);
        }else{
            // Resume from where the player left off (by index or class-string)
            $this->tutorialSequence->startFrom($player, $progress);
        }
    }
}

Termination reasons

terminate(string $reason) accepts any string. Built-in constants are provided by SessionTerminateReasons:

Constant Value Description
MANUAL "manual" Explicitly terminated by plugin code
PLAYER_QUIT "player_quit" Player disconnected
PLUGIN_DISABLE "plugin_disable" Owning plugin was disabled
START_FAILED "start_failed" Session failed to initialize
COMPLETED "completed" Session reached its end state
CANCELLED "cancelled" Session abandoned before completion
TIMEOUT "timeout" Session exceeded allotted time
RESTART "restart" Session terminated to restart fresh
MAINTENANCE "maintenance" Server maintenance

Installation

See Official Poggit Virion Documentation.


License

Distributed under the MIT License. See LICENSE for more information.


About

General-purpose player session management library

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • PHP 100.0%