Inventory-based window menu library for PocketMine-MP that creates fully client-side fake container windows with fluent configuration, click/close handling, layout helpers, pagination, and a NetworkStackLatency-based retry mechanism for reliable delivery.
- 8 Container Types: Chest (27), Double Chest (54), Hopper (5), Barrel (27), Furnace (3), Blast Furnace (3), Smoker (3), Brewing Stand (5)
- Two Authoring Styles: Fluent/closure-based via
DynamicWindowand class-based viaWindowsubclasses - Read-Only Mode: Cancels all item movement while still firing click handlers
- Transaction Control: Cancel, allow, or schedule post-transaction callbacks through
WindowTransaction - Fill Helpers:
fill(),fillSlots(),fillAll(),fillBorder(),clearAll() - Live Refresh: Re-sync inventory contents while the window is already open
- Container Properties: Control furnace progress bars, brewing stand animations via
setProperty() - Inventory Binding: Bidirectional sync between the window inventory and any external inventory
- Layout System: Borders, rows, navigation bars, centered rows, checkerboard patterns, and named slot groups
- Pagination: Automatic page splitting with configurable navigation buttons
- Confirmation Preset: Ready-made yes/no dialog with customizable items and callbacks
- Client-Side Graphics: Fake blocks, tile entities, paired chests, and invisible minecart entities
- NSL Retry Mechanism: Automatic retry (up to 20 attempts) using
NetworkStackLatencyPacketas ACK - Per-Player Sessions: Tracks window state, graphic lifecycle, and network timing per player
- Custom Type Registry: Register your own
WindowTypeimplementations - Platform-Aware Networking: Per-device timestamp encoding (handles PlayStation differences)
Requires PocketMine-MP 5.0.0+ and PHP 8.2+.
Place the LibWindow folder in your server's plugins/ directory. It loads at STARTUP and initializes automatically.
Add to your plugin.yml:
depend:
- LibWindowLibWindow initializes itself on enable. If you need manual initialization from another plugin:
use imperazim\window\WindowManager;
WindowManager::init($this);- Added
Windowabstract class -- base for class-based window menus. Extend it and overridestructure(),onClick(), andonClose(). The window opens automatically on construction. Provides fluent proxy methods (setName(),readonly(),fill(),fillSlots(),fillAll(),fillBorder(),clearAll(),bindInventory(),refresh(),setProperty()) and access to the underlyingDynamicWindow,Inventory,Player, and custom data. - Added
DynamicWindowclass -- fluent/closure-based API for inline window creation. Static factories:chest(),doubleChest(),hopper(),barrel(),furnace(),blastFurnace(),smoker(),brewingStand(), and the genericcreate(). SupportssetName(),readonly(),onClick(),onClose(),fill(),fillSlots(),fillAll(),fillBorder(),clearAll(),bindInventory(),refresh(),setProperty(), andsendTo(). - Added
WindowManagerclass -- central manager that initializes the system, registers event listeners, manages the built-in type registry, and routes packet/inventory events. CallWindowManager::init($plugin)once. Access the type registry viagetTypeRegistry()and player sessions viagetSession($player). - Added
ChestWindowabstract class --Windowpre-configured for a single chest (27 slots). - Added
DoubleChestWindowabstract class --Windowpre-configured for a double chest (54 slots). - Added
HopperWindowabstract class --Windowpre-configured for a hopper (5 slots). - Added
BarrelWindowabstract class --Windowpre-configured for a barrel (27 slots). - Added
FurnaceWindowabstract class --Windowpre-configured for a furnace (3 slots). - Added
BlastFurnaceWindowabstract class --Windowpre-configured for a blast furnace (3 slots). - Added
SmokerWindowabstract class --Windowpre-configured for a smoker (3 slots). - Added
BrewingStandWindowabstract class --Windowpre-configured for a brewing stand (5 slots).
Fluent/closure-based approach with DynamicWindow:
use imperazim\window\DynamicWindow;
use imperazim\window\transaction\WindowTransaction;
use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
DynamicWindow::chest("Shop")
->fill(0, VanillaItems::DIAMOND_SWORD()->setCustomName("Warrior Blade"))
->fill(1, VanillaItems::BOW()->setCustomName("Hunter's Bow"))
->readonly()
->onClick(function (WindowTransaction $tx): void {
$player = $tx->getPlayer();
$item = $tx->getItemClicked();
$player->sendMessage("You selected: " . $item->getCustomName());
$player->removeCurrentWindow();
})
->onClose(function (Player $player): void {
$player->sendMessage("Thanks for visiting the shop!");
})
->sendTo($player);Class-based approach extending a typed Window subclass:
use imperazim\window\ChestWindow;
use imperazim\window\transaction\WindowTransaction;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
class KitSelectorWindow extends ChestWindow {
protected function structure(Player $player, ?array $data): void {
$this->setName("Select a Kit");
$this->readonly();
$this->fill(11, VanillaItems::IRON_SWORD()->setCustomName("Warrior"));
$this->fill(13, VanillaItems::BOW()->setCustomName("Archer"));
$this->fill(15, VanillaItems::SPLASH_POTION()->setCustomName("Healer"));
$this->fillBorder(
VanillaBlocks::STAINED_GLASS_PANE()->asItem()->setCustomName(" ")
);
}
protected function onClick(WindowTransaction $tx): void {
$player = $tx->getPlayer();
match ($tx->getSlot()) {
11 => $player->sendMessage("Warrior kit selected"),
13 => $player->sendMessage("Archer kit selected"),
15 => $player->sendMessage("Healer kit selected"),
default => null,
};
$player->removeCurrentWindow();
}
protected function onClose(Player $player): void {
$player->sendMessage("Kit selection closed.");
}
}
// Opens automatically on construction
new KitSelectorWindow($player);
// Pass custom data via the second argument
new KitSelectorWindow($player, ["category" => "pvp"]);Other container types with DynamicWindow:
use imperazim\window\DynamicWindow;
use pocketmine\item\VanillaItems;
// Hopper (5 slots)
DynamicWindow::hopper("Pick a Difficulty")
->fill(0, VanillaItems::FEATHER()->setCustomName("Easy"))
->fill(2, VanillaItems::IRON_INGOT()->setCustomName("Normal"))
->fill(4, VanillaItems::BLAZE_ROD()->setCustomName("Hard"))
->readonly()
->sendTo($player);
// Barrel (27 slots)
DynamicWindow::barrel("Loot Barrel")->fillSlots($items)->readonly()->sendTo($player);
// Furnace (3 slots) with progress bar
$window = DynamicWindow::furnace("Smelting");
$window->fill(0, VanillaItems::IRON_ORE()->setCustomName("Input"))
->fill(2, VanillaItems::IRON_INGOT()->setCustomName("Output"))
->readonly()
->sendTo($player, function (bool $success) use ($window, $player): void {
if ($success) {
$window->setProperty($player, 1, 200);
}
});
// Blast Furnace, Smoker, Brewing Stand
DynamicWindow::blastFurnace("Alloy Forge")->readonly()->sendTo($player);
DynamicWindow::smoker("Food Processor")->readonly()->sendTo($player);
DynamicWindow::brewingStand("Potion Lab")->readonly()->sendTo($player);- Added
PaginatedWindowclass -- pre-built paginated window for browsing large item lists. Uses a double chest by default. Supports configurable items per page, navigation slot positions, custom nav button items, border items, click/close handlers, and read-only mode. Internally usesPaginationfor page splitting and automatic navigation. - Added
ConfirmWindowclass -- pre-built confirmation dialog with confirm (slot 11) and cancel (slot 15) items in a single chest. Always read-only. Supports custom confirm/cancel/filler items andonConfirm()/onCancel()callbacks. Closing the window triggers the cancel callback.
Paginated item browser:
use imperazim\window\preset\PaginatedWindow;
use imperazim\window\transaction\WindowTransaction;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
$allItems = []; // Large array of Item objects
$glass = VanillaBlocks::STAINED_GLASS_PANE()->asItem()->setCustomName(" ");
PaginatedWindow::create("All Items", $allItems)
->setItemsPerPage(45)
->setNavSlots(45, 53)
->setBorderItem($glass)
->setPrevItem(VanillaItems::ARROW()->setCustomName("Previous Page"))
->setNextItem(VanillaItems::ARROW()->setCustomName("Next Page"))
->readonly()
->onClick(function (WindowTransaction $tx): void {
$tx->getPlayer()->sendMessage("Selected: " . $tx->getItemClicked()->getName());
})
->onClose(function (Player $player): void {
$player->sendMessage("Browser closed.");
})
->sendTo($player);Confirmation dialog:
use imperazim\window\preset\ConfirmWindow;
use pocketmine\block\VanillaBlocks;
use pocketmine\player\Player;
ConfirmWindow::create("Delete this warp?")
->setConfirmItem(
VanillaBlocks::EMERALD()->asItem()->setCustomName("Yes, delete it")
)
->setCancelItem(
VanillaBlocks::REDSTONE()->asItem()->setCustomName("No, keep it")
)
->setFillerItem(
VanillaBlocks::STAINED_GLASS_PANE()->asItem()->setCustomName(" ")
)
->onConfirm(function (Player $player): void {
$player->sendMessage("Warp deleted.");
})
->onCancel(function (Player $player): void {
$player->sendMessage("Deletion cancelled.");
})
->sendTo($player);- Added
Layoutclass -- pre-defined layout configurations:bordered()fills the border and places items in the inner area,rows()places items by row index,navBar()fills the last row with navigation items and optional filler,centerRow()centers items horizontally in a specific row. - Added
Patternclass -- visual fill patterns:border()fills border slots,inner()fills inner slots,fill()fills all slots,checkerboard()alternates two items,row()fills a single row,group()fills aSlotGroupwith one item,groupItems()distributes an item array across aSlotGroup. - Added
SlotGroupclass -- named group of inventory slot indices. Factory methods:range()for consecutive slots,of()for explicit indices,border()for border slots of a chest-style inventory,inner()for inner (non-border) slots,row()for a specific row,column()for a specific column. - Added
Paginationclass -- automatic page splitting for item lists. HandlesapplyPage()to place items and navigation buttons,handleClick()to detect nav button clicks and switch pages. Configurable items per page, start slot, navigation slots, custom prev/next/filler items.
use imperazim\window\DynamicWindow;
use imperazim\window\layout\Layout;
use imperazim\window\layout\Pattern;
use imperazim\window\layout\SlotGroup;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\VanillaItems;
$window = DynamicWindow::doubleChest("Organized Menu");
$glass = VanillaBlocks::STAINED_GLASS_PANE()->asItem()->setCustomName(" ");
// Bordered layout with content items in the inner area
Layout::bordered($window, $glass, $contentItems);
// Row-based layout
Layout::rows($window, [
0 => [VanillaItems::DIAMOND(), VanillaItems::EMERALD()],
2 => [VanillaItems::GOLD_INGOT(), VanillaItems::IRON_INGOT()],
]);
// Center items in a specific row
Layout::centerRow($window, 1, [
VanillaItems::NETHER_STAR()->setCustomName("Special Item"),
]);
// Navigation bar in the last row
Layout::navBar($window, [
0 => VanillaItems::ARROW()->setCustomName("Back"),
8 => VanillaItems::ARROW()->setCustomName("Next"),
], $glass);
// Checkerboard pattern
Pattern::checkerboard($window, $glass, VanillaItems::AIR());
// Named slot groups
$innerSlots = SlotGroup::inner(54);
$borderSlots = SlotGroup::border(54);
$thirdRow = SlotGroup::row(2);
$middleColumn = SlotGroup::column(4, 6);
$custom = SlotGroup::of("hotbar", [0, 1, 2, 3, 4, 5, 6, 7, 8]);
// Fill a slot group with one item
Pattern::group($window, $borderSlots, $glass);
// Distribute items across a slot group
Pattern::groupItems($window, $innerSlots, $itemArray);Using Pagination directly:
use imperazim\window\DynamicWindow;
use imperazim\window\layout\Pagination;
use imperazim\window\transaction\WindowTransaction;
use pocketmine\item\VanillaItems;
$window = DynamicWindow::doubleChest("Browse Items");
$pagination = new Pagination(
items: $allItems,
itemsPerPage: 45,
startSlot: 0,
prevSlot: 45,
nextSlot: 53,
);
$pagination->applyPage($window, 0);
$window->readonly()
->onClick(function (WindowTransaction $tx) use ($pagination, $window): void {
if ($pagination->handleClick($window, $tx)) {
return; // Navigation button was clicked
}
// Handle normal item click
})
->sendTo($player);- Added
WindowInventoryclass -- customSimpleInventorythat implementsBlockInventoryviaBlockInventoryTrait. Associates the inventory with a fake block position that is updated when the graphic is placed. Constructor takes the slot count. - Added
InventoryBridgeclass -- manages bidirectional synchronization between a window's internal inventory and an external inventory. On construction, copies external contents into internal and attaches listeners in both directions. Calldestroy()to remove listeners. - Added
InventorySyncclass --InventoryListenerthat forwards slot and content changes from one inventory to another. Uses asyncingflag andpause()/resume()to prevent infinite feedback loops during bidirectional sync.
use imperazim\window\DynamicWindow;
use pocketmine\inventory\SimpleInventory;
// Bind an external inventory for bidirectional sync
$sharedInventory = new SimpleInventory(27);
DynamicWindow::chest("Shared Storage")
->bindInventory($sharedInventory)
->sendTo($player);
// Changes in the window reflect in $sharedInventory and vice versa- Added
WindowTransactionclass -- represents a player's click on a window slot. ProvidesgetPlayer(),getItemClicked()(item in the slot),getItemClickedWith()(item on cursor),getSlot(),getAction(), andgetTransaction(). By default the transaction is allowed; callcancel()to prevent item movement,allow()to re-enable it. Callthen()to schedule a post-transaction callback that fires after the inventory state settles (queued through the NSL system).
use imperazim\window\transaction\WindowTransaction;
->onClick(function (WindowTransaction $tx): void {
$player = $tx->getPlayer();
$clicked = $tx->getItemClicked();
$cursor = $tx->getItemClickedWith();
$slot = $tx->getSlot();
$tx->cancel();
// Schedule a callback after the transaction is processed
$tx->then(function () use ($player): void {
$player->sendMessage("Action completed.");
});
})- Added
WindowSessionclass -- per-player session state. Tracks the currently openWindowInfo, manages the graphic lifecycle (send/remove), and coordinates the NSL retry loop for reliable window delivery. Exposes theWindowNetworkinstance for async operations. HandlessetCurrentWindow()which sends the graphic, waits for an NSL round-trip, opens the inventory, and enters the retry loop if the client rejects the container. - Added
SessionManagerclass -- managesWindowSessionlifecycle. Creates sessions onPlayerLoginEvent(selecting the appropriateNetworkHandlerbased on device OS) and destroys them onPlayerQuitEvent. Providesget()andgetOrThrow()for session access. - Added
WindowInfoclass -- value object holding aDynamicWindow, itsWindowGraphic, and an optional custom name. Represents the state of a currently displayed window menu.
use imperazim\window\WindowManager;
use pocketmine\player\Player;
// Get a player's session
$session = WindowManager::getSession($player);
// Check if a window is currently open
$current = $session?->getCurrent();
if ($current !== null) {
$dynamicWindow = $current->window;
$graphic = $current->graphic;
}
// Remove the current window programmatically
$session?->removeCurrentWindow();- Added
WindowNetworkclass -- async operation queue usingNetworkStackLatencyPacketas an ACK mechanism. The Bedrock client responds to each NSL packet with a matching timestamp, which acts as a "ready signal" for sequencing operations. Provideswait()for immediate async operations,waitUntil()for timed waits,notify()to match incoming timestamps,dropAll()anddropOfType()to cancel pending operations, andhookContainerOpen()to intercept and translateContainerOpenPacketthrough the graphic's packet translator. - Added
NetworkHandlerinterface -- createsLatencyEntryinstances with platform-specific timestamp encoding. Different platforms (e.g., PlayStation) encode NSL timestamps differently. - Added
NetworkHandlerRegistryclass -- maps device OS toNetworkHandlerimplementations. Includes a default handler (timestamp * 1000000) and a PlayStation-specific handler (timestamp * 1000). - Added
LatencyEntryclass -- value object representing a queued async operation. Holds the delay type, the sent timestamp, the network-encoded timestamp, and the callback closure. The callback returnstrueto repeat the operation (another NSL round-trip) orfalseto finish.
The networking layer is managed automatically by WindowSession and WindowManager. Direct interaction is rarely needed, but it is possible:
use imperazim\window\WindowManager;
// Access the network handler registry to register a custom platform handler
$registry = WindowManager::getSessionManager()->getHandlerRegistry();
$registry->register($customDeviceOs, $customHandler);
// Access a player's network queue
$session = WindowManager::getSession($player);
$pending = $session->network->getPending();- Added
WindowGraphicinterface -- defines the contract for client-side visual elements:send()to place the graphic,sendInventory()to open the inventory on it,remove()to clean up,getPacketTranslator()forContainerOpenPacketinterception, andgetAnimationDuration()for open animation timing. - Added
PositionedGraphicinterface -- extendsWindowGraphicwithgetPosition()for block-based graphics that have a world position. - Added
BlockActorGraphicclass -- sends a fake block with tile entity NBT (used for chest, hopper, barrel, furnace, etc.). On removal, restores the real world block at that position. - Added
BlockGraphicclass -- sends a plain fake block without tile data. Used as a barrier to prevent visual merging between fake and real chests on adjacent positions. - Added
MultiBlockGraphicclass -- composite graphic managing multiplePositionedGraphicinstances. Delegates inventory and translation to the primary (first) graphic. Used for double chests (two paired blocks plus barrier blocks). - Added
PairedChestGraphicclass -- sends a fake chest block withTAG_PAIRX/TAG_PAIRZNBT to link two chests for a double chest window. - Added
MinecartChestGraphicclass -- entity-based graphic that spawns an invisible chest minecart at the player's position. Useful when placing fake blocks is not viable (e.g., in the void or mid-air).
Graphics are created automatically by WindowType implementations. For custom window types, you can construct them directly:
use imperazim\window\graphic\BlockActorGraphic;
use imperazim\window\graphic\MinecartChestGraphic;
use imperazim\window\graphic\MultiBlockGraphic;
use imperazim\window\graphic\BlockGraphic;
use imperazim\window\graphic\translator\BlockTranslator;
use pocketmine\block\VanillaBlocks;
use pocketmine\math\Vector3;
// Single block actor graphic
$graphic = new BlockActorGraphic(
new Vector3(100, 64, 200),
VanillaBlocks::CHEST(),
"Chest",
new BlockTranslator(),
);
// Entity-based graphic for void/air scenarios
$graphic = new MinecartChestGraphic(
$player->getPosition(),
);
// Composite with barriers for double chest
$primary = new BlockActorGraphic($pos, VanillaBlocks::CHEST(), "Chest", new BlockTranslator());
$barrier = new BlockGraphic($adjacentPos, VanillaBlocks::BARRIER());
$composite = new MultiBlockGraphic($primary, $barrier);- Added
PacketTranslatorinterface -- translates aContainerOpenPacketin-place to point to the correct fake block/entity. Used to rewrite block position, actor ID, or window type. - Added
BlockTranslatorclass -- rewrites theContainerOpenPacketblock position to match the graphic'sPositionedGraphic::getPosition(). - Added
CompositeTranslatorclass -- chains multiplePacketTranslatorinstances sequentially. Applies each translator in order. - Added
WindowTypeTranslatorclass -- overwrites the window type field inContainerOpenPacket. Used for non-chest containers (hopper, furnace, blast furnace, smoker, brewing stand) that require a specific UI type constant.
Translators are composed automatically by BlockActorWindowType. For custom types:
use imperazim\window\graphic\translator\BlockTranslator;
use imperazim\window\graphic\translator\WindowTypeTranslator;
use imperazim\window\graphic\translator\CompositeTranslator;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
// Single translator
$translator = new BlockTranslator();
// Composite: rewrite position + window type
$translator = new CompositeTranslator(
new BlockTranslator(),
new WindowTypeTranslator(WindowTypes::HOPPER),
);- Added
WindowTypeinterface -- defines a window menu type. Each implementation providescreateInventory()to create the internal inventory,createGraphic()to create the client-side graphic for a player, andgetSize()for the slot count. - Added
WindowTypeIdsinterface -- string constants for all built-in window type identifiers:TYPE_CHEST,TYPE_DOUBLE_CHEST,TYPE_HOPPER,TYPE_BARREL,TYPE_FURNACE,TYPE_BLAST_FURNACE,TYPE_SMOKER,TYPE_BREWING_STAND. - Added
WindowTypeRegistryclass -- maps string identifiers toWindowTypeinstances. Providesregister(),get(),has(), andgetIds(). All built-in types are registered automatically duringWindowManager::init(). - Added
BlockActorWindowTypeclass -- creates a single fake block with tile entity. Used for chest, hopper, barrel, furnace, blast furnace, smoker, and brewing stand. When the block is a chest, barrier blocks are automatically placed on adjacent real chests to prevent visual merging. ComposesBlockTranslatorand optionallyWindowTypeTranslatorfor non-default window types. - Added
DoubleBlockWindowTypeclass -- creates two paired chest blocks side by side withPairedChestGraphicfor a 54-slot double chest. Places barriers on adjacent real chests.
use imperazim\window\WindowManager;
use imperazim\window\type\WindowType;
use imperazim\window\type\WindowTypeIds;
use imperazim\window\graphic\WindowGraphic;
use imperazim\window\graphic\MinecartChestGraphic;
use imperazim\window\inventory\WindowInventory;
use pocketmine\inventory\Inventory;
use pocketmine\player\Player;
// Register a custom window type
$registry = WindowManager::getTypeRegistry();
$registry->register("myPlugin:minecart_chest", new class implements WindowType {
public function createInventory(): Inventory {
return new WindowInventory(27);
}
public function createGraphic(Player $player): ?WindowGraphic {
return new MinecartChestGraphic($player->getPosition());
}
public function getSize(): int {
return 27;
}
});
// Use the custom type
use imperazim\window\DynamicWindow;
DynamicWindow::create("myPlugin:minecart_chest")
->setName("Minecart Storage")
->readonly()
->sendTo($player);LibWindow uses NetworkStackLatencyPacket (NSL) as an acknowledgment system to guarantee reliable window delivery. The full sequence is:
- The fake graphic (block or entity) is sent to the client.
- An NSL packet with a unique timestamp is sent. The system waits for the client to echo back the matching timestamp, confirming it processed the graphic.
- Once acknowledged, the inventory is opened via
ContainerOpenPacket. TheWindowNetworkhooks into PocketMine'sContainerOpenCallbacksto intercept and translate the packet (rewriting position, window type) through the graphic'sPacketTranslator. - The system enters a retry loop: another NSL packet is sent. If the client accepted the window, no
ContainerClosePacketarrives during the round-trip, and the open is confirmed. - If the client rejects the container (sends a
ContainerClosePacketwith window ID 255/-1), the system retries by resendingContainerClose+ContainerOpen+ inventory contents, then waits for another NSL round-trip. - This repeats for up to 20 attempts. If all retries fail or the player disconnects, the window is cleaned up and the callback receives
false.
The NetworkHandlerRegistry ensures platform-specific timestamp encoding is handled correctly (e.g., PlayStation echoes timestamp * 1000 instead of the standard format). Developers never need to interact with this system directly -- all windows automatically benefit from reliable delivery.
This project is licensed under MIT. Please see the LICENSE file for details.
LibWindow now publishes an EasyLibrary 3.x internal package asset for the migration from standalone-only libraries to installable internal packages.
Release assets now include:
LibWindow-2.0.0.pharLibWindow-2.0.0.easylib.zippackage.ymlchecksums.txt
The standalone plugin remains supported. During the EasyLibrary 3.x migration, if the standalone plugin is installed on a server, EasyLibrary marks the internal package as shadowed and does not autoload it.