Do not steal chat with a global AsyncChatEvent / ChatEvent listener.
DevTool replaced that hack with Paper ConversationFactory after
PR #1 merged into
rework/v2 at
72ff53e.
Read the merged implementation:
StartConversation— per-player prompt,withModality(false), escapecancelPromptWorldInput— world name / border (positive int) / seed (long)ConfigEditorMenu— string config keys use the sameStartConversation.ask(…)
In-game clicks: DevTool
docs/examples/in-game.md.
ConversationFactory is deprecated-for-removal on Paper 26.2 (Dialogs replace
it later). It is still the API DevTool and MockBukkit use on this target.
Drop this helper next to your CorePlugin subclass. It matches DevTool’s
contract: not modal (other chat is not captured unless this player is in
the prompt), 60s timeout, type cancel (or time out) to abort.
package com.example.arena.prompt;
import com.github.pinont.singularitylib.plugin.CorePlugin;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.ConversationFactory;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import java.util.function.Consumer;
@SuppressWarnings({"deprecation", "removal"})
public final class AskPlayer {
public static final String ESCAPE = "cancel";
private AskPlayer() {
}
/**
* Closes the current inventory and begins a modal-off chat prompt.
* Type {@code cancel} (or wait out the timeout) to abort.
*/
public static Conversation ask(Player player, String promptText,
Consumer<String> onAnswer, Runnable onCancel) {
Plugin plugin = CorePlugin.getInstance();
player.closeInventory();
ConversationFactory factory = new ConversationFactory(plugin)
.withModality(false)
.withLocalEcho(true)
.withTimeout(60)
.withEscapeSequence(ESCAPE)
.withPrefix(context -> "Arena » ")
.thatExcludesNonPlayersWithMessage("Players only.")
.withFirstPrompt(new StringPrompt() {
@Override
public String getPromptText(ConversationContext context) {
return promptText;
}
@Override
public Prompt acceptInput(ConversationContext context, String input) {
if (input == null || input.isBlank()) {
return this;
}
onAnswer.accept(input.trim());
return Prompt.END_OF_CONVERSATION;
}
})
.addConversationAbandonedListener(event -> {
if (!event.gracefulExit() && onCancel != null) {
onCancel.run();
}
});
Conversation conversation = factory.buildConversation(player);
conversation.begin();
return conversation;
}
}Same shape as DevTool’s World Creator. Close the GUI, prompt, parse, reopen:
AskPlayer.ask(player,
"Please send a world name into chat (or type cancel).",
name -> player.sendMessage("World name: " + name),
() -> player.sendMessage("Cancelled."));
AskPlayer.ask(player,
"Please send a world border size into chat (or type cancel).",
input -> {
try {
int parsed = Integer.parseInt(input);
if (parsed <= 0) {
player.sendMessage("World border size must be greater than 0");
return;
}
player.sendMessage("Border: " + parsed);
} catch (NumberFormatException e) {
player.sendMessage("World border size must be a number.");
}
},
() -> player.sendMessage("Cancelled."));
AskPlayer.ask(player,
"Please send a seed number into chat (or type cancel).",
input -> {
try {
long parsed = Long.parseLong(input);
player.sendMessage("Seed: " + parsed);
} catch (NumberFormatException e) {
player.sendMessage("World seed must be a number.");
}
},
() -> player.sendMessage("Cancelled."));Blank input re-prompts (return this). cancel fires the abandoned listener
with gracefulExit() == false, which runs onCancel. A valid answer ends the
conversation (Prompt.END_OF_CONVERSATION) and does not run onCancel.
withModality(true) blocks all other chat and commands for that player until
the prompt ends. DevTool uses false so only the conversation’s own
messages are captured; everyone else (and this player, when not prompting)
keeps a normal chat pipeline. There is no plugin-wide chat listener.