> For the complete documentation index, see [llms.txt](https://docs.marioded.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.marioded.com/api-how-to-guides/hotbar-controls.md).

# Custom hotbar controls

Add a playback action, validate its parameters, and bind it to a hotbar item.

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

```java
ReversePlayer player = ReversePlayer.get().orElseThrow();
LayoutService layout = player.getLayoutService();
```

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

```java
layout.registerAction("example:rewind_round", (context, parameters) -> {
    context.playbackService().seekTo(context.session(), 0);
});
```

`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())` if your control should explicitly resume after seeking.

## Bind it from configuration

Merge an item under `playing.items` in `layout.yml`:

```yaml
playing:
  items:
    rewind_round:
      enabled: true
      slot: 8
      appearance:
        material: ARROW
        data: 0
        amount: 1
        name: '<yellow>Rewind round'
        lore: ['<gray>Return to the beginning.']
      states: {}
      bindings:
        LEFT:
          action: example:rewind_round
          parameters: {}
```

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:

```java
layout.registerAction("example:set_speed", new LayoutAction() {
    @Override
    public void validate(Map<String, String> parameters) {
        if (!parameters.keySet().equals(Set.of("speed"))) {
            throw new IllegalArgumentException("Expected only a speed parameter");
        }
        double speed = Double.parseDouble(parameters.get("speed"));
        if (!Double.isFinite(speed) || speed <= 0) {
            throw new IllegalArgumentException("Speed must be finite and positive");
        }
    }

    @Override
    public void execute(LayoutItem.Context context, Map<String, String> parameters) {
        context.session().setSpeed(Double.parseDouble(parameters.get("speed")));
    }
});
```

`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`:

```java
layout.setItem(new LayoutItem(
    "example:speed_item",
    8,
    context -> new ItemStack(Material.FEATHER),
    Map.of(LayoutItem.Click.LEFT,
        new LayoutItem.Binding("example:set_speed", Map.of("speed", "0.5")))));
```

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(viewer, key, value)` provides viewer defaults that can survive a configuration reload. Passing null removes an existing default; removing one that does not exist can throw.

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.

{% hint style="info" %}
Per-viewer item state is not a separate playback. Seeking, pause, and speed still affect every viewer sharing that session.
{% endhint %}

## Reload and disable

Programmatic items survive layout reloads and can shadow configured keys. Remove your item explicitly:

```java
layout.removeItem("example:speed_item");
```

The layout API has no action-unregister method. Retire your own callback with a noncapturing inert action when disabling:

```java
layout.registerAction("example:set_speed", (context, parameters) -> {});
layout.registerAction("example:rewind_round", (context, parameters) -> {});
```

Do this only for your registrations. Intentionally replacing a shared ID requires coordination with other integrations. See the [complete UI example](/api-how-to-guides/examples/ui-extensions.md) for setup and teardown in a JavaPlugin.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.marioded.com/api-how-to-guides/hotbar-controls.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
