Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ dependencies:

## Usage
To use SingularityLib, your main class must extend `CorePlugin` instead of `JavaPlugin`.

Copy-paste examples (CommandGroup, Paper conversations, Item Studio export) live in
[`docs/examples/`](docs/examples/README.md). The GitHub wiki is not published; those
markdown pages are the docs home.
```java
public class Main extends CorePlugin {
@Override
Expand Down
24 changes: 24 additions & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Singularity examples

Copy-pasteable consumer snippets for SingularityLib **2.0** (Paper 26.2+, JDK 25).
The GitHub wiki for this repo is not published (`Pinont/SingularityLib.wiki` 404s),
so these pages are the docs home.

| Page | What it covers |
| --- | --- |
| [CommandGroup](command-group.md) | Root command + `SubCommand` dispatch, aliases, help, registration |
| [Conversations](conversations.md) | Paper `ConversationFactory` prompts (`withModality(false)`, type `cancel` to abort) |
| [Export snippets](export-snippets.md) | Item Studio / Entity Studio Java export → `ItemCreator` / `CustomItem` |

In-game flows (Item Studio, World Creator prompts, click-to-copy) live in
[Singularity-DevTool](https://github.com/Pinont/Singularity-DevTool) on
`rework/v2` after [PR #1](https://github.com/Pinont/Singularity-DevTool/pull/1)
(`72ff53e`). See that repo’s
[`docs/examples/`](https://github.com/Pinont/Singularity-DevTool/tree/rework/v2/docs/examples)
for the menu clicks.

**API notes these examples assume:**

- Package `com.github.pinont.singularitylib` (Maven coordinates stay `io.github.pinont:singularitylib`).
- `new ItemCreator(CorePlugin.getInstance(), Material.…)` — the Plugin argument is required in 2.x.
- Consumer plugins extend `CorePlugin` and register components with `registerComponents(…)` or `@AutoRegister`.
200 changes: 200 additions & 0 deletions docs/examples/command-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# CommandGroup

`CommandGroup` is a `SimpleCommand` that owns a map of `SubCommand`s. With no
arguments it prints a gold/yellow help listing. With a first argument it
dispatches to the matching subcommand and passes **the remaining args**.

Verified against
[`CommandGroup.java`](https://github.com/Pinont/SingularityLib/blob/main/src/main/java/com/github/pinont/singularitylib/api/command/CommandGroup.java)
and
[`SubCommand.java`](https://github.com/Pinont/SingularityLib/blob/main/src/main/java/com/github/pinont/singularitylib/api/command/SubCommand.java)
on **`main`**. Production usage:
[`DevToolCommand`](https://github.com/Pinont/Singularity-DevTool/blob/72ff53e941eb34636f12f54e769945abe3acebfe/src/main/java/com/github/pinont/devtool/commands/DevToolCommand.java)
on DevTool `rework/v2` (`72ff53e`).

## Register a group

`CommandGroup` is a `SimpleCommand`, so it registers the same way as any other
command. Prefer explicit registration from `onPluginStart()`:

```java
package com.example.arena;

import com.github.pinont.singularitylib.plugin.CorePlugin;

public class ArenaPlugin extends CorePlugin {

@Override
public void onPluginStart() {
registerComponents(new ArenaCommand());
}

@Override
public void onPluginStop() {
}
}
```

`getName()` may list aliases with colons. `CommandManager` splits on `:` and
registers each token. `"arena:ar"` becomes `/arena` and `/ar`.

You can also mark the group `@AutoRegister` (no-arg constructor required) if
your build runs `singularitylib-processor`. DevTool itself uses
`registerComponents(new DevToolCommand())` instead of a scan.

## Copy-paste: root + two subcommands

```java
package com.example.arena;

import com.github.pinont.singularitylib.api.command.CommandGroup;
import com.github.pinont.singularitylib.api.command.SubCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class ArenaCommand extends CommandGroup {

public ArenaCommand() {
registerSubcommand(new CreateSub());
registerSubcommand(new DeleteSub());
registerSubcommand(new ListSub());
}

@Override
public String getName() {
return "arena:ar";
}
}

final class CreateSub extends SubCommand {

@Override
public String getName() {
return "create:c";
}

@Override
public String getDescription() {
return "Create an arena";
}

@Override
public String getPermission() {
return "arena.create";
}

@Override
public boolean isPlayerOnly() {
return true;
}

@Override
public void execute(CommandSender sender, String[] args) {
Player player = (Player) sender;
if (args.length < 1) {
player.sendMessage(Component.text("Usage: /arena create <name>", NamedTextColor.YELLOW));
return;
}
player.sendMessage(Component.text("Created arena " + args[0], NamedTextColor.GREEN));
}
}

final class DeleteSub extends SubCommand {

@Override
public String getName() {
return "delete";
}

@Override
public String getDescription() {
return "Delete an arena";
}

@Override
public String getPermission() {
return "arena.delete";
}

@Override
public void execute(CommandSender sender, String[] args) {
if (args.length < 1) {
sender.sendMessage(Component.text("Usage: /arena delete <name>", NamedTextColor.YELLOW));
return;
}
sender.sendMessage(Component.text("Deleted arena " + args[0], NamedTextColor.RED));
}
}

final class ListSub extends SubCommand {

@Override
public String getName() {
return "list:ls";
}

@Override
public String getDescription() {
return "List arenas";
}

@Override
public void execute(CommandSender sender, String[] args) {
sender.sendMessage(Component.text("Arenas: (none yet)", NamedTextColor.GRAY));
}
}
```

## Behaviour to expect

| Input | Result |
| --- | --- |
| `/arena` | Auto help: `—— arena:ar help ——` then one yellow line per subcommand (`getName()` is printed as-is, including aliases) |
| `/arena create spawn` | `CreateSub.execute` with `args = ["spawn"]` (subcommand name stripped) |
| `/arena c spawn` | Same — `create:c` registers both `create` and `c` |
| `/arena nope` | Red `Unknown subcommand: nope. Use /arena:ar help` |
| Console `/arena create spawn` | Red `This subcommand is players-only.` (`isPlayerOnly()`) |
| No permission | Red `You do not have permission to use: /arena:ar create:c` |

Empty `getPermission()` / `null` means no permission check. Empty
`getDescription()` omits the ` — …` suffix on the help line.

The sender-based `execute(CommandSender, String[])` path is what tests should
call (see `CommandGroupTest` in this repo). Paper still enters through
`execute(CommandSourceStack, String[])`, which forwards to the sender overload.

## Override the root (DevTool pattern)

If no-args should **not** print help, override `execute(CommandSender, String[])`
and only call `super.execute` for known subcommands. DevTool does this so
`/devtool` opens a GUI and `/devtool itemstudio` still dispatches:

```java
@Override
public void execute(CommandSender sender, String[] args) {
if (args.length == 0) {
sender.sendMessage(Component.text("Open the menu, or /arena help", NamedTextColor.YELLOW));
return;
}
if (getSubcommandNames().contains(args[0].toLowerCase())) {
super.execute(sender, args);
return;
}
sender.sendMessage(Component.text("Unknown: " + args[0], NamedTextColor.RED));
}
```

`getSubcommandNames()` returns every registered key (primary names **and**
aliases), in insertion order.

## What CommandGroup does not do

- It does not implement Brigadier/tab suggestions. `SimpleCommand` extends
Paper `BasicCommand`; add `suggest(CommandSourceStack, String[])` yourself if
you want subcommand completion.
- `paper-plugin.yml` has no `commands:` block in the bootstrap model. Registration
is programmatic via `CommandManager` / `LifecycleEvents.COMMANDS`.
- Subcommand classes are **not** `SimpleCommand`s. Do not `@AutoRegister` a
`SubCommand` by itself — register it on a `CommandGroup`.
144 changes: 144 additions & 0 deletions docs/examples/conversations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Conversations

Do not steal chat with a global `AsyncChatEvent` / `ChatEvent` listener.
DevTool replaced that hack with Paper `ConversationFactory` after
[PR #1](https://github.com/Pinont/Singularity-DevTool/pull/1) merged into
`rework/v2` at
[`72ff53e`](https://github.com/Pinont/Singularity-DevTool/commit/72ff53e941eb34636f12f54e769945abe3acebfe).

Read the merged implementation:

- [`StartConversation`](https://github.com/Pinont/Singularity-DevTool/blob/72ff53e941eb34636f12f54e769945abe3acebfe/src/main/java/com/github/pinont/devtool/methods/StartConversation.java)
— per-player prompt, `withModality(false)`, escape `cancel`
- [`PromptWorldInput`](https://github.com/Pinont/Singularity-DevTool/blob/72ff53e941eb34636f12f54e769945abe3acebfe/src/main/java/com/github/pinont/devtool/methods/PromptWorldInput.java)
— world name / border (positive int) / seed (long)
- [`ConfigEditorMenu`](https://github.com/Pinont/Singularity-DevTool/blob/72ff53e941eb34636f12f54e769945abe3acebfe/src/main/java/com/github/pinont/devtool/menu/submenu/ConfigEditorMenu.java)
— string config keys use the same `StartConversation.ask(…)`

In-game clicks: DevTool
[`docs/examples/in-game.md`](https://github.com/Pinont/Singularity-DevTool/blob/rework/v2/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.

## Copy-paste: minimal consumer prompt

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.

```java
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;
}
}
```

## World-name / border / seed (PromptWorldInput pattern)

Same shape as DevTool’s World Creator. Close the GUI, prompt, parse, reopen:

```java
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`.

## Why `withModality(false)`

`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.
Loading
Loading