Skip to main content

Custom hotbar controls

Let’s add a control that rewinds the replay. Then we will improve it with a parameterized action and a rendered item. All LayoutService operations run on the Bukkit main thread.

Get the layout service

Use the real player service. It already manages the temporary viewer inventory, so our integration only needs to add an action or item.

Register a simple action

context.session() is the current playback session. seekTo rebuilds/resynchronizes the scene; setting the cursor field directly would not do that work. This action preserves the API seek’s pause behavior. Add context.playbackService().resume(context.session().getSessionId()) if your control should explicitly resume after seeking.

Bind it from configuration

Merge an item under playing.items in layout.yml:
Reload the player configuration and test the item while watching. Hotbar slots are 1–9. Slot 8 is free in the shipped defaults, but check owner customizations before using it.

Improve an action with parameters

Suppose the owner wants to choose a speed in each binding. Register a LayoutAction with validation:
validate runs without changing state. It rejects missing/unknown keys or unusable values before an edit is published and before execution. execute performs the actual change. Do not mutate a session or register another item from validation. A YAML binding can now use action: example:set_speed with parameters: {speed: '0.5'}. Unknown action IDs may be configured before an integration registers them; registration later validates existing bindings.

Or install an item in Java

For a control with dynamic rendering, create a LayoutItem:
Use this instead of the previous slot-8 YAML item, or choose another free slot. The service rejects a collision without publishing a partial layout. The renderer receives a viewer/session context and returns an ItemStack. Reverse handles placement and stack cloning. Supported bindings are LEFT, RIGHT, SHIFT_LEFT, and SHIFT_RIGHT.

Remember a viewer’s choice

context.state() belongs to one viewer and one item. Use it for choices such as a selected interval, and do not retain the context beyond the callback. addDefaultState(viewerId, key, value) provides viewer defaults that can survive a configuration reload. Pass the viewer UUID (viewer.getUniqueId()). Passing null removes an existing default; removing one that does not exist can throw. clearViewerState(viewerId) clears all state for that viewer. The renderer should use context data. A paused session may skip redraws while nothing relevant changes, so call layout.refresh(viewer) when your external application state changes. Refresh still accepts an actual Player or PlaybackSession, rather than a UUID.
Per-viewer item state is not a separate playback. Seeking, pause, and speed still affect every viewer sharing that session.

Reload and disable

Programmatic items survive layout reloads and can shadow configured keys. Remove your item explicitly:
Unregister your actions when disabling:
Do this only for your registrations. Intentionally replacing a shared ID requires coordination with other integrations. See the complete UI example for setup and teardown in a JavaPlugin.