A serverside user interface library for Minecraft: Java Edition
—
Book creation and management for opening written books with multiple pages, authors, and titles. Adventure provides comprehensive book handling for interactive content delivery.
Core interface for creating and managing written books that can be opened by players.
/**
* Represents a book with title, author, and pages
*/
interface Book extends Examinable {
/**
* Gets the book title
* @return the title component
*/
Component title();
/**
* Gets the book author
* @return the author component
*/
Component author();
/**
* Gets the book pages
* @return list of page components
*/
List<Component> pages();
/**
* Sets the book title
* @param title the new title
* @return book with new title
*/
Book title(ComponentLike title);
/**
* Sets the book author
* @param author the new author
* @return book with new author
*/
Book author(ComponentLike author);
/**
* Sets the book pages
* @param pages the new pages
* @return book with new pages
*/
Book pages(List<? extends ComponentLike> pages);
/**
* Sets the book pages from varargs
* @param pages the pages
* @return book with new pages
*/
Book pages(ComponentLike... pages);
/**
* Adds a page to the book
* @param page the page to add
* @return book with added page
*/
Book addPage(ComponentLike page);
/**
* Creates a book with title, author, and pages
* @param title the book title
* @param author the book author
* @param pages the book pages
* @return new book
*/
static Book book(ComponentLike title, ComponentLike author, ComponentLike... pages);
/**
* Creates a book with title, author, and page list
* @param title the book title
* @param author the book author
* @param pages the book pages
* @return new book
*/
static Book book(ComponentLike title, ComponentLike author, List<? extends ComponentLike> pages);
/**
* Creates a book builder
* @return new book builder
*/
static Builder book();
/**
* Builder for creating books
*/
interface Builder extends AbstractBuilder<Book> {
/**
* Sets the book title
* @param title the title
* @return this builder
*/
Builder title(ComponentLike title);
/**
* Sets the book author
* @param author the author
* @return this builder
*/
Builder author(ComponentLike author);
/**
* Sets the book pages
* @param pages the pages
* @return this builder
*/
Builder pages(List<? extends ComponentLike> pages);
/**
* Sets the book pages from varargs
* @param pages the pages
* @return this builder
*/
Builder pages(ComponentLike... pages);
/**
* Adds a page to the book
* @param page the page to add
* @return this builder
*/
Builder addPage(ComponentLike page);
}
}Usage Examples:
import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
// Simple book
Book simpleBook = Book.book(
Component.text("My Book"),
Component.text("Author Name"),
Component.text("This is page 1 of my book."),
Component.text("This is page 2 with more content.")
);
// Open book for player
audience.openBook(simpleBook);
// Complex book with formatting
Book storyBook = Book.book()
.title(Component.text("The Adventure", NamedTextColor.GOLD, TextDecoration.BOLD))
.author(Component.text("Jane Doe", NamedTextColor.BLUE))
.addPage(Component.text()
.append(Component.text("Chapter 1", NamedTextColor.DARK_GREEN, TextDecoration.UNDERLINED))
.appendNewline()
.appendNewline()
.append(Component.text("Once upon a time, in a land far away..."))
.build())
.addPage(Component.text()
.append(Component.text("Chapter 2", NamedTextColor.DARK_GREEN, TextDecoration.UNDERLINED))
.appendNewline()
.appendNewline()
.append(Component.text("The hero continued their journey..."))
.build())
.build();
audience.openBook(storyBook);public class HelpBookSystem {
public Book createHelpBook(String topic) {
return switch (topic.toLowerCase()) {
case "commands" -> createCommandsBook();
case "rules" -> createRulesBook();
case "features" -> createFeaturesBook();
default -> createMainHelpBook();
};
}
private Book createMainHelpBook() {
return Book.book()
.title(Component.text("Server Help", NamedTextColor.BLUE, TextDecoration.BOLD))
.author(Component.text("Server Staff"))
.addPage(createTableOfContents())
.addPage(createGettingStartedPage())
.addPage(createContactInfoPage())
.build();
}
private Component createTableOfContents() {
return Component.text()
.append(Component.text("Help Topics", NamedTextColor.DARK_BLUE, TextDecoration.BOLD))
.appendNewline()
.appendNewline()
.append(Component.text("• Getting Started", NamedTextColor.BLACK))
.appendNewline()
.append(Component.text("• Commands (/help commands)", NamedTextColor.BLUE)
.clickEvent(ClickEvent.runCommand("/help commands")))
.appendNewline()
.append(Component.text("• Rules (/help rules)", NamedTextColor.BLUE)
.clickEvent(ClickEvent.runCommand("/help rules")))
.appendNewline()
.append(Component.text("• Features (/help features)", NamedTextColor.BLUE)
.clickEvent(ClickEvent.runCommand("/help features")))
.build();
}
private Book createCommandsBook() {
List<Component> pages = new ArrayList<>();
// Command categories
pages.add(createCommandCategoryPage("Basic Commands", getBasicCommands()));
pages.add(createCommandCategoryPage("Economy Commands", getEconomyCommands()));
pages.add(createCommandCategoryPage("Social Commands", getSocialCommands()));
return Book.book()
.title(Component.text("Command Reference"))
.author(Component.text("Server"))
.pages(pages)
.build();
}
private Component createCommandCategoryPage(String category, List<CommandInfo> commands) {
Component.Builder page = Component.text()
.append(Component.text(category, NamedTextColor.DARK_GREEN, TextDecoration.BOLD))
.appendNewline()
.appendNewline();
for (CommandInfo cmd : commands) {
page.append(Component.text("/" + cmd.name(), NamedTextColor.BLUE)
.clickEvent(ClickEvent.suggestCommand("/" + cmd.name())))
.appendNewline()
.append(Component.text(cmd.description(), NamedTextColor.BLACK))
.appendNewline()
.appendNewline();
}
return page.build();
}
}public class LoreBookManager {
private final Map<String, Book> loreBooks = new HashMap<>();
public void createLoreCollection() {
// Create interconnected story books
loreBooks.put("origin", createOriginStory());
loreBooks.put("prophecy", createProphecyBook());
loreBooks.put("heroes", createHeroesBook());
loreBooks.put("bestiary", createBestiaryBook());
}
private Book createOriginStory() {
return Book.book()
.title(Component.text("The Origin", NamedTextColor.GOLD, TextDecoration.ITALIC))
.author(Component.text("Ancient Scribe"))
.addPage(createStoryPage(
"In the beginning...",
"Long before the great kingdoms rose, there was only the Void. " +
"From this emptiness came the first spark of creation, " +
"giving birth to the world as we know it."
))
.addPage(createStoryPage(
"The First Age",
"The First Age saw the rise of the Elder Beings, " +
"creatures of immense power who shaped the very fabric of reality. " +
"Their wars scarred the land and created the great rifts " +
"that still exist today."
))
.addPage(createNavigationPage())
.build();
}
private Component createStoryPage(String title, String content) {
return Component.text()
.append(Component.text(title, NamedTextColor.DARK_PURPLE, TextDecoration.BOLD))
.appendNewline()
.appendNewline()
.append(Component.text(content, NamedTextColor.BLACK))
.build();
}
private Component createNavigationPage() {
return Component.text()
.append(Component.text("Related Stories", NamedTextColor.BLUE, TextDecoration.UNDERLINED))
.appendNewline()
.appendNewline()
.append(Component.text("• The Prophecy", NamedTextColor.DARK_BLUE)
.clickEvent(ClickEvent.runCommand("/lore prophecy"))
.hoverEvent(HoverEvent.showText(Component.text("Click to read the ancient prophecy"))))
.appendNewline()
.append(Component.text("• Heroes of Old", NamedTextColor.DARK_BLUE)
.clickEvent(ClickEvent.runCommand("/lore heroes"))
.hoverEvent(HoverEvent.showText(Component.text("Learn about legendary heroes"))))
.appendNewline()
.append(Component.text("• Bestiary", NamedTextColor.DARK_BLUE)
.clickEvent(ClickEvent.runCommand("/lore bestiary"))
.hoverEvent(HoverEvent.showText(Component.text("Catalog of creatures"))))
.build();
}
}public class DynamicBookGenerator {
public Book createPlayerStatsBook(PlayerStats stats) {
return Book.book()
.title(Component.text("Player Statistics"))
.author(Component.text("Server"))
.addPage(createStatsOverviewPage(stats))
.addPage(createAchievementsPage(stats))
.addPage(createLeaderboardPage(stats))
.build();
}
private Component createStatsOverviewPage(PlayerStats stats) {
return Component.text()
.append(Component.text("Your Statistics", NamedTextColor.DARK_GREEN, TextDecoration.BOLD))
.appendNewline().appendNewline()
.append(createStatLine("Play Time", formatDuration(stats.playTime())))
.append(createStatLine("Blocks Broken", String.valueOf(stats.blocksBroken())))
.append(createStatLine("Distance Walked", stats.distanceWalked() + " blocks"))
.append(createStatLine("Mobs Killed", String.valueOf(stats.mobsKilled())))
.append(createStatLine("Deaths", String.valueOf(stats.deaths())))
.append(createStatLine("Level", String.valueOf(stats.level())))
.build();
}
private Component createStatLine(String label, String value) {
return Component.text()
.append(Component.text(label + ": ", NamedTextColor.GRAY))
.append(Component.text(value, NamedTextColor.WHITE))
.appendNewline();
}
public Book createServerInfoBook(ServerInfo info) {
return Book.book()
.title(Component.text("Server Information"))
.author(Component.text("Administration"))
.addPage(createServerStatsPage(info))
.addPage(createOnlinePlayersPage(info))
.addPage(createServerRulesPage(info))
.build();
}
private Component createOnlinePlayersPage(ServerInfo info) {
Component.Builder page = Component.text()
.append(Component.text("Online Players", NamedTextColor.BLUE, TextDecoration.BOLD))
.appendNewline()
.append(Component.text(info.onlineCount() + "/" + info.maxPlayers(), NamedTextColor.GREEN))
.appendNewline().appendNewline();
for (String player : info.onlinePlayers()) {
page.append(Component.text("• " + player, NamedTextColor.YELLOW)
.clickEvent(ClickEvent.suggestCommand("/msg " + player + " "))
.hoverEvent(HoverEvent.showText(Component.text("Click to message " + player))))
.appendNewline();
}
return page.build();
}
}public class BookValidator {
private static final int MAX_PAGE_LENGTH = 256;
private static final int MAX_PAGES = 50;
private static final int MAX_TITLE_LENGTH = 32;
private static final int MAX_AUTHOR_LENGTH = 32;
public boolean validateBook(Book book) {
// Validate title length
if (getPlainTextLength(book.title()) > MAX_TITLE_LENGTH) {
return false;
}
// Validate author length
if (getPlainTextLength(book.author()) > MAX_AUTHOR_LENGTH) {
return false;
}
// Validate page count
if (book.pages().size() > MAX_PAGES) {
return false;
}
// Validate each page
for (Component page : book.pages()) {
if (getPlainTextLength(page) > MAX_PAGE_LENGTH) {
return false;
}
if (!validatePageContent(page)) {
return false;
}
}
return true;
}
private boolean validatePageContent(Component page) {
// Check for malicious content, inappropriate links, etc.
PlainTextComponentSerializer serializer = PlainTextComponentSerializer.plainText();
String plainText = serializer.serialize(page);
// Basic content validation
return !containsInappropriateContent(plainText) &&
!containsMaliciousLinks(page);
}
private int getPlainTextLength(Component component) {
return PlainTextComponentSerializer.plainText().serialize(component).length();
}
public Book sanitizeBook(Book book) {
Component sanitizedTitle = sanitizeComponent(book.title(), MAX_TITLE_LENGTH);
Component sanitizedAuthor = sanitizeComponent(book.author(), MAX_AUTHOR_LENGTH);
List<Component> sanitizedPages = book.pages().stream()
.limit(MAX_PAGES)
.map(page -> sanitizeComponent(page, MAX_PAGE_LENGTH))
.collect(Collectors.toList());
return Book.book()
.title(sanitizedTitle)
.author(sanitizedAuthor)
.pages(sanitizedPages)
.build();
}
private Component sanitizeComponent(Component component, int maxLength) {
String plainText = PlainTextComponentSerializer.plainText().serialize(component);
if (plainText.length() <= maxLength) {
return component;
}
// Truncate while preserving formatting
return Component.text(plainText.substring(0, maxLength - 3) + "...")
.mergeStyle(component);
}
}public class PaginatedBookBuilder {
private static final int LINES_PER_PAGE = 14;
private static final int CHARS_PER_LINE = 20; // Approximate
public Book createPaginatedBook(String title, String author, List<String> content) {
List<Component> pages = new ArrayList<>();
Component.Builder currentPage = Component.text();
int currentLines = 0;
for (String line : content) {
if (currentLines >= LINES_PER_PAGE) {
pages.add(currentPage.build());
currentPage = Component.text();
currentLines = 0;
}
// Handle long lines by wrapping
List<String> wrappedLines = wrapText(line, CHARS_PER_LINE);
for (String wrappedLine : wrappedLines) {
if (currentLines >= LINES_PER_PAGE) {
pages.add(currentPage.build());
currentPage = Component.text();
currentLines = 0;
}
currentPage.append(Component.text(wrappedLine)).appendNewline();
currentLines++;
}
}
if (currentLines > 0) {
pages.add(currentPage.build());
}
return Book.book()
.title(Component.text(title))
.author(Component.text(author))
.pages(pages)
.build();
}
private List<String> wrapText(String text, int maxWidth) {
List<String> lines = new ArrayList<>();
String[] words = text.split(" ");
StringBuilder currentLine = new StringBuilder();
for (String word : words) {
if (currentLine.length() + word.length() + 1 > maxWidth) {
if (currentLine.length() > 0) {
lines.add(currentLine.toString());
currentLine = new StringBuilder();
}
}
if (currentLine.length() > 0) {
currentLine.append(" ");
}
currentLine.append(word);
}
if (currentLine.length() > 0) {
lines.add(currentLine.toString());
}
return lines;
}
}Install with Tessl CLI
npx tessl i tessl/maven-net-kyori--adventure-api