> For the complete documentation index, see [llms.txt](https://cyr1en.gitbook.io/commandprompter/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cyr1en.gitbook.io/commandprompter/developers/custom-screens.md).

# Custom Screens

CommandPrompter 3.3.0 introduces a public service API for third-party plugins to provide prompt screens. A registered key such as `ecoitem` turns `<ecoitem:Choose a weapon>` into a normal CommandPrompter step whose answer comes from your `InputScreen`.

The integration uses three public types from `prompt-ui-api`:

* `CommandPrompterAPI` registers and unregisters providers.
* `PromptScreenFactory` creates one un-opened screen per prompt step.
* `ScreenContext` supplies the key, display text, custom flags, and sanitation mode.

## Add the API dependency

Use the same API version as the CommandPrompter plugin installed on the server. The custom-screen service starts in 3.3.0.

### Gradle Kotlin DSL

```kotlin
val commandPrompterVersion = "3.3.0"

repositories {
    mavenCentral()
    maven("https://repo.papermc.io/repository/maven-public/")
    maven("https://repo.cyr1en.dev/snapshots")
}

dependencies {
    compileOnly("io.papermc.paper:paper-api:26.1.2.build.74-stable")
    compileOnly("dev.cyr1en:prompt-ui-api:$commandPrompterVersion")
}
```

### Gradle Groovy DSL

```groovy
repositories {
    mavenCentral()
    maven 'https://repo.papermc.io/repository/maven-public/'
    maven 'https://repo.cyr1en.dev/snapshots'
}

dependencies {
    compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
    compileOnly 'dev.cyr1en:prompt-ui-api:3.3.0'
}
```

### Maven

```xml
<repositories>
  <repository>
    <id>cyr1en-snapshots</id>
    <url>https://repo.cyr1en.dev/snapshots</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>dev.cyr1en</groupId>
    <artifactId>prompt-ui-api</artifactId>
    <version>3.3.0</version>
    <scope>provided</scope>
  </dependency>
</dependencies>
```

{% hint style="warning" %}
Use `compileOnly`/`provided`. Do not shade or relocate `dev.cyr1en.promptui.*`; CommandPrompter supplies those classes at runtime. The custom-screen API requires CommandPrompter and `prompt-ui-api` 3.3.0 or newer; the released 3.2.x API does not contain this service.
{% endhint %}

CommandPrompter and its API are built for Java 25.

## Declare the plugin dependency

For `plugin.yml`:

```yaml
name: ExampleScreens
version: 1.0.0
main: com.example.screens.ExampleScreensPlugin
api-version: '1.21'
depend: [CommandPrompterPaper]
```

Use `softdepend` instead when the integration is optional.

For `paper-plugin.yml`:

```yaml
name: ExampleScreens
version: 1.0.0
main: com.example.screens.ExampleScreensPlugin
api-version: '1.21'
dependencies:
  server:
    CommandPrompterPaper:
      load: BEFORE
      required: true
```

The server dependency ensures that CommandPrompter has published its Bukkit service before your `onEnable()` runs.

## Obtain the service and register a key

```java
package com.example.screens;

import dev.cyr1en.promptui.api.CommandPrompterAPI;
import org.bukkit.plugin.java.JavaPlugin;

public final class ExampleScreensPlugin extends JavaPlugin {
    private CommandPrompterAPI commandPrompter;

    @Override
    public void onEnable() {
        var registration = getServer()
                .getServicesManager()
                .getRegistration(CommandPrompterAPI.class);

        if (registration == null) {
            getLogger().warning("CommandPrompterAPI is unavailable; custom screens were not registered.");
            return;
        }

        commandPrompter = registration.getProvider();
        commandPrompter.registerScreen(
                this,
                "material_picker",
                (player, context) -> new MaterialPickerScreen(this, player, context));
    }

    @Override
    public void onDisable() {
        if (commandPrompter != null) {
            commandPrompter.unregisterScreens(this);
        }
    }
}
```

Explicit unregistration is safe but optional on ordinary plugin disable: CommandPrompter listens for provider disable, removes every key owned by that plugin, cancels its active prompt sessions, and attempts to close inventory UIs.

## Key rules

Registration keys:

* Must match `^[a-z][a-z0-9_]{0,31}$`.
* Must be lowercase when registered; command tag lookup is case-insensitive.
* Are globally unique while registered.
* Cannot collide with `screen-mappings` in `prompt-config.yml`.
* Cannot use built-in keys: the empty Chat key, `a`, `anvil`, `s`, `sign`, `p`, `player`, `d`, `dialog`, `c`, `confirm`, `confirmation`, `i`, `item`, or the `@` preset namespace.

An invalid or duplicate key throws `IllegalArgumentException`. Registration while either plugin is disabled or CommandPrompter is shutting down throws `IllegalStateException`.

## Implement `InputScreen`

CommandPrompter calls a screen in this order:

1. `PromptScreenFactory.createScreen(player, context)`
2. `screen.onResult(callback)`
3. `screen.onOpenFailure(callback)`
4. `screen.open()`
5. One result callback, or a later programmatic `close()`

This example displays one material and returns its namespaced key:

```java
package com.example.screens;

import dev.cyr1en.promptui.InputScreen;
import dev.cyr1en.promptui.ScreenResult;
import dev.cyr1en.promptui.api.ScreenContext;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

public final class MaterialPickerScreen implements InputScreen, Listener {
    private final Plugin plugin;
    private final Player player;
    private final ScreenContext context;
    private final AtomicBoolean completed = new AtomicBoolean();

    private Consumer<ScreenResult> resultCallback;
    private Consumer<Throwable> openFailureCallback;
    private Inventory inventory;
    private boolean open;

    public MaterialPickerScreen(Plugin plugin, Player player, ScreenContext context) {
        this.plugin = plugin;
        this.player = player;
        this.context = context;
    }

    @Override
    public void onResult(Consumer<ScreenResult> callback) {
        resultCallback = callback;
    }

    @Override
    public void onOpenFailure(Consumer<Throwable> callback) {
        openFailureCallback = callback;
    }

    @Override
    public void open() {
        try {
            var title = MiniMessage.miniMessage().deserialize(context.displayText());
            inventory = Bukkit.createInventory(null, 9, title);
            inventory.setItem(4, new ItemStack(Material.DIAMOND_SWORD));

            Bukkit.getPluginManager().registerEvents(this, plugin);
            open = true;
            player.openInventory(inventory);
        } catch (Throwable error) {
            open = false;
            HandlerList.unregisterAll(this);
            if (openFailureCallback != null) {
                openFailureCallback.accept(error);
            }
        }
    }

    @Override
    public void close() {
        if (!open) return;
        open = false;
        HandlerList.unregisterAll(this);
        if (player.getOpenInventory().getTopInventory().equals(inventory)) {
            player.closeInventory();
        }
    }

    @Override
    public boolean isOpen() {
        return open;
    }

    @EventHandler
    public void onClick(InventoryClickEvent event) {
        if (!open || !event.getWhoClicked().equals(player)) return;
        if (!event.getView().getTopInventory().equals(inventory)) return;

        event.setCancelled(true);
        if (event.getClickedInventory() == null
                || !event.getClickedInventory().equals(inventory)
                || event.getSlot() != 4) {
            return;
        }

        complete(ScreenResult.answer("minecraft:diamond_sword"), true);
    }

    @EventHandler
    public void onDrag(InventoryDragEvent event) {
        if (!open || !event.getWhoClicked().equals(player)) return;
        if (!event.getView().getTopInventory().equals(inventory)) return;
        event.setCancelled(true);
    }

    @EventHandler
    public void onClose(InventoryCloseEvent event) {
        if (!open || !event.getPlayer().equals(player)) return;
        if (!event.getView().getTopInventory().equals(inventory)) return;
        complete(ScreenResult.guiExit(), false);
    }

    private void complete(ScreenResult result, boolean closeInventory) {
        if (!completed.compareAndSet(false, true)) return;

        open = false;
        HandlerList.unregisterAll(this);
        if (closeInventory && player.getOpenInventory().getTopInventory().equals(inventory)) {
            player.closeInventory();
        }

        if (resultCallback != null) {
            resultCallback.accept(result);
        }
    }
}
```

Return `ScreenResult.answer(value)` for a successful answer. Common cancellation factories are `ScreenResult.guiExit()`, `manualCancel()`, `timeout()`, and `error()`. A programmatic `close()` must close resources without delivering a second result.

CommandPrompter reschedules provider results onto the player's entity scheduler, discards stale callbacks, and enforces an internal exactly-once guard. Providers should still guard their own UI events, unregister listeners, and deliver no more than one result.

## Read display text and custom flags

A provider tag may add arbitrary trailing flags:

```
/reward <material_picker:Choose a weapon -glow -rarity:legendary -description:"Event reward">
```

The factory receives:

| Method                                      | Result                                                                |
| ------------------------------------------- | --------------------------------------------------------------------- |
| `context.key()`                             | Canonical lowercase key.                                              |
| `context.displayText()`                     | Display text with standard and custom flags removed.                  |
| `context.flags()`                           | Immutable custom flag map.                                            |
| `context.flag("rarity")`                    | Optional case-insensitive lookup.                                     |
| `context.hasFlag("glow")`                   | Whether a flag exists.                                                |
| `context.booleanFlag("glow")`               | `true` when its value parses as `true`; a bare flag has value `true`. |
| `context.flagOrDefault("rarity", "common")` | Value or a fallback.                                                  |
| `context.sanitize()`                        | `false` when the standard `-ds` flag disabled answer sanitation.      |

Custom flags must be trailing tokens. Names start with a letter, contain only letters, digits, or underscores, and are at most 32 characters. Values are at most 512 characters, and one tag may contain at most 16 flags with at most 1024 total value characters. Duplicate flags and malformed quotes fail closed.

## Threading and lifecycle contract

* `createScreen`, `open`, and normal `close` calls run on the target player's entity scheduler. Return promptly and never perform blocking database, file, or network work there.
* Return a new, non-null, unopened screen from the factory; keep factory work short.
* A provider may deliver its result from any thread. CommandPrompter hops back to the entity scheduler and re-verifies the active session before accepting it.
* Exceptions from the factory, `open()`, or a result path cancel only that session with an error.
* When a provider disables, CommandPrompter does not invoke the provider's `InputScreen.close()` because its classes may be unloading. It makes a best-effort platform inventory close and cancels the session instead.
* Providers remain responsible for non-inventory resources such as protocol screens, dialogs, database subscriptions, or external GUI state during their own disable path.

## Failure behavior

Unknown or unregistered tag keys abort interception and log an administrative diagnostic. They never fall back to Chat. The same is true if a provider unregisters between command authoring and execution.

| Problem                                       | Check                                                                                                                                  |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| API service is `null`                         | Confirm the dependency declaration loads CommandPrompter first and both plugins are enabled.                                           |
| `NoClassDefFoundError` or class-cast failures | Remove shaded/relocated API classes and use `compileOnly`/`provided`.                                                                  |
| Key rejected                                  | Use the lowercase grammar and check built-in, configured, and other provider keys.                                                     |
| Prompt aborts after open                      | Ensure `open()` returns promptly, callbacks are single-shot, and initialization failures use `onOpenFailure`.                          |
| UI remains after provider disable             | Close non-inventory/protocol resources in the provider's own disable cleanup; host cleanup is best-effort for Bukkit inventories only. |


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://cyr1en.gitbook.io/commandprompter/developers/custom-screens.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
