### Start Local Development Server
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/README.md
Starts a local development server for live preview. Changes are reflected without restarting.
```bash
$ yarn start
```
--------------------------------
### Install Project Dependencies
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/README.md
Installs project dependencies using Yarn. Run this command after cloning the repository.
```bash
$ yarn
```
--------------------------------
### Setup Custom Dropdowns
Source: https://github.com/feathermc/feather-server-api/blob/main/examples/shop-html/src/shop.html
Initializes the behavior for custom dropdown menus, including toggling visibility on click and closing when clicking outside the dropdown.
```javascript
setupCustomDropdowns() {
document.addEventListener('click', (event) => {
if (!event.target.closest('.custom-dropdown')) {
document.querySelectorAll('.custom-dropdown').forEach(dropdown => {
dropdown.classList.remove('dropdown-open');
const selected = dropdown.querySelector('.dropdown-selected');
if (selected) selected.setAttribute('aria-expanded', 'false');
});
return;
}
if (event.target.classList.contains('dropdown-selected') || event.target.closest('.dropdown-selected')) {
const dropdown = event.target.closest('.custom-dropdown');
this.toggleDropdown(dropdown);
return;
}
if (event.target.classList.contains('dro
```
--------------------------------
### Setup Back Button Handler
Source: https://github.com/feathermc/feather-server-api/blob/main/examples/shop-html/src/shop.html
Configures event listeners for a 'back to categories' button, allowing navigation via click or keyboard (Enter/Space).
```javascript
setupBackButtonHandler() {
const backButton = document.getElementById('back-to-categories');
if (!backButton) return;
const handler = (event) => {
if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') {
if (event.type !== 'click') event.preventDefault();
backToCategories();
}
};
backButton.addEventListener('click', handler);
backButton.addEventListener('keydown', handler);
}
```
--------------------------------
### Making RPC Calls from the Client
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Example JavaScript code demonstrating how to make an RPC call to the server using the fetch API and handle the JSON response.
```APIDOC
## Making RPC Calls from the Client
```javascript
async function buyItem(itemId, quantity) {
try {
const response = await fetch(`https://${window.resourceName}/buyItem`, {
method: 'POST',
body: JSON.stringify({
itemId: itemId,
quantity: quantity
})
});
const result = await response.json();
if (result.success) {
showSuccessMessage(`Successfully purchased ${quantity} items!`);
} else {
showErrorMessage(result.error || 'Failed to purchase items');
}
} catch (error) {
showErrorMessage('Network error, please try again');
console.error(error);
}
}
// Call the function when a button is clicked
document.getElementById('buy-button').addEventListener('click', () => {
const itemId = parseInt(document.getElementById('item-id').value);
const quantity = parseInt(document.getElementById('quantity').value);
buyItem(itemId, quantity);
});
```
```
--------------------------------
### Maven Dependency Setup
Source: https://context7.com/feathermc/feather-server-api/llms.txt
Add the Feather Maven repository and declare the API as a compile-only dependency. The backing plugin is provided at runtime.
```xml
feather-repohttps://repo.feathermc.net/artifactory/maven-releasesnet.digitalingot.feather-server-apiapi0.0.5provided
```
--------------------------------
### Handle Server List Background Exceptions
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
This example shows how to catch and handle specific exceptions that can occur when setting a server list background, such as unsupported formats, oversized images, or invalid image data.
```java
try {
ServerListBackgroundFactory factory = metaService.getServerListBackgroundFactory();
ServerListBackground background = factory.byPath(imagePath);
metaService.setServerListBackground(background);
} catch (UnsupportedImageFormatException e) {
logger.error("Image format not supported. Only PNG is supported.", e);
} catch (ImageSizeExceededException e) {
logger.error("Image is too large. Maximum dimensions: 1009×202, Maximum size: 512KB", e);
} catch (InvalidImageException e) {
logger.error("Image file is corrupted or invalid.", e);
} catch (IOException e) {
logger.error("Error reading image file.", e);
}
```
--------------------------------
### Load Server Background on Startup
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
Implement this in your plugin's onEnable method to load a custom server background when the plugin starts. It ensures the backgrounds directory exists and loads the configured background file.
```java
@Override
public void onEnable() {
// Create the backgrounds directory if it doesn't exist
Path backgroundsDir = getDataFolder().toPath().resolve("backgrounds");
try {
Files.createDirectories(backgroundsDir);
} catch (IOException e) {
getLogger().severe("Failed to create backgrounds directory");
e.printStackTrace();
}
// Load the configured background
String backgroundFile = getConfig().getString("background", "default.png");
loadBackground(backgroundFile);
}
private void loadBackground(String filename) {
try {
Path imagePath = getDataFolder().toPath().resolve("backgrounds").resolve(filename);
if (Files.exists(imagePath)) {
ServerListBackground background = FeatherAPI.getMetaService()
.getServerListBackgroundFactory()
.byPath(imagePath);
FeatherAPI.getMetaService().setServerListBackground(background);
getLogger().info("Server background loaded: " + filename);
} else {
getLogger().warning("Background file not found: " + filename);
}
} catch (Exception e) {
getLogger().severe("Error loading server background");
e.printStackTrace();
}
}
```
--------------------------------
### Highlight Event Locations
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/waypoints.md
Create temporary waypoints to indicate event locations for all online players. This example uses a 5-minute duration and the 'chroma' color effect.
```java
WaypointBuilder builder = waypointService.createWaypointBuilder(eventX, eventY, eventZ)
.withName("Treasure Hunt")
.withColor(WaypointColor.chroma()) // Use the special chroma effect for important events
.withDuration(WaypointDuration.of(300)); // 5 minutes in seconds
for (FeatherPlayer player : FeatherAPI.getPlayerService().getPlayers()) {
waypointService.createWaypoint(player, builder);
}
```
--------------------------------
### Gradle (Groovy) Dependency Setup
Source: https://context7.com/feathermc/feather-server-api/llms.txt
Configure Gradle to use the Feather Maven repository and add the API as a compile-only dependency. The runtime dependency is handled by the server plugin.
```groovy
repositories {
maven {
name = 'feather-repo'
url = 'https://repo.feathermc.net/artifactory/maven-releases'
}
}
dependencies {
compileOnly 'net.digitalingot.feather-server-api:api:0.0.5'
}
```
--------------------------------
### Setup Item Event Delegation
Source: https://github.com/feathermc/feather-server-api/blob/main/examples/shop-html/src/shop.html
Attaches click and keydown event listeners to the items container for efficient handling of buy and sell actions. Includes logic for stopping propagation and identifying the target item and action.
```javascript
setupItemEventDelegation() {
if (!DOM.itemsContainer) return;
DOM.itemsContainer.removeEventListener('click', this._itemClickHandler);
DOM.itemsContainer.removeEventListener('keydown', this._itemKeyHandler);
this._itemClickHandler = (event) => {
if (event.target.classList.contains('buy-button') && !event.target.disabled) {
event.stopPropagation();
const itemElement = event.target.closest('.shop-item');
if (itemElement) {
handleItemAction(itemElement, 'buy');
}
} else if (event.target.classList.contains('sell-button') && !event.target.disabled) {
event.stopPropagation();
const itemElement = event.target.closest('.shop-item');
if (itemElement) {
handleItemAction(itemElement, 'sell');
}
}
};
this._itemKeyHandler = (event) => {
if (!event.target.classList.contains('shop-item')) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
const buyButton = event.target.querySelector('.buy-button:not([disabled])');
if (buyButton) {
handleItemAction(event.target, 'buy');
return;
}
const sellButton = event.target.querySelector('.sell-button:not([disabled])');
if (sellButton) {
handleItemAction(event.target, 'sell');
}
}
};
DOM.itemsContainer.addEventListener('click', this._itemClickHandler);
DOM.itemsContainer.addEventListener('keydown', this._itemKeyHandler);
}
```
--------------------------------
### Detecting Players with Feather
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/events.md
Provides a practical example of using the `PlayerHelloEvent` to detect when players join the server with Feather, and to retrieve information about their platform and enabled mods.
```APIDOC
## Detecting Players with Feather
### Description
One of the most common use cases is detecting when players join with Feather.
### Code Example
```java
eventService.subscribe(PlayerHelloEvent.class, event -> {
FeatherPlayer player = event.getPlayer();
Platform platform = event.getPlatform();
Collection mods = event.getFeatherMods();
getLogger().info(player.getName() + " joined with Feather on " + platform);
getLogger().info("Enabled mods: " + mods.stream()
.map(FeatherMod::getName)
.collect(Collectors.joining(", ")));
});
```
```
--------------------------------
### Gradle (Kotlin DSL) Dependency Setup
Source: https://context7.com/feathermc/feather-server-api/llms.txt
Set up the Feather Maven repository and declare the API as a compile-only dependency using Kotlin DSL for Gradle. The server plugin provides the runtime dependency.
```kotlin
repositories {
maven {
name = "feather-repo"
url = uri("https://repo.feathermc.net/artifactory/maven-releases")
}
}
dependencies {
compileOnly("net.digitalingot.feather-server-api:api:0.0.5")
}
```
--------------------------------
### Basic RPC Controller Structure
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Example of a Java RPC controller class with a handler method for buying items. It demonstrates parsing JSON requests, processing actions, and sending JSON responses.
```APIDOC
## Basic Controller Structure
```java
public class ShopController implements RpcController {
@RpcHandler("buyItem")
public void buyItem(RpcRequest request, RpcResponse response) {
// Get the player
FeatherPlayer player = request.getSource();
// Parse the request body (usually JSON)
String requestBody = request.getBody();
JsonObject json = new Gson().fromJson(requestBody, JsonObject.class);
// Process the request
int itemId = json.get("itemId").getAsInt();
int quantity = json.get("quantity").getAsInt();
// Perform the action
boolean success = processPurchase(player, itemId, quantity);
// Send a response
JsonObject responseJson = new JsonObject();
responseJson.addProperty("success", success);
if (!success) {
responseJson.addProperty("error", "Not enough coins");
}
response.respond(new Gson().toJson(responseJson));
}
private boolean processPurchase(FeatherPlayer player, int itemId, int quantity) {
// Implementation details
return true; // Return success or failure
}
}
```
```
--------------------------------
### Asynchronous World Generation with Delayed Response
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
This example demonstrates how to handle a long-running world generation task asynchronously. The RPC handler responds immediately with the processing status and sends the actual result later using `response.respond()`. This method must be called within a 30-second timeout.
```APIDOC
## generateWorld RPC Handler
### Description
Handles asynchronous world generation requests, responding with the result after the operation completes. The response must be sent within 30 seconds.
### Method
`generateWorld(RpcRequest request, RpcResponse response)`
### Parameters
- `request` (RpcRequest): The incoming RPC request object.
- `response` (RpcResponse): The RPC response object to send the result back.
### Request Body Example
```json
{
"worldName": "myNewWorld"
}
```
### Response
#### Success Response
- `success` (boolean): Indicates if the world generation was successful.
- `worldName` (string): The name of the generated world.
#### Error Response
- `success` (boolean): Always `false` in case of an error.
- `error` (string): A message describing the error.
### Response Example (Success)
```json
{
"success": true,
"worldName": "myNewWorld"
}
```
### Response Example (Error)
```json
{
"success": false,
"error": "Failed to generate world: Invalid name"
}
```
### Important Note
The `response.respond()` method must be called within 30 seconds of receiving the request. After this timeout, any responses will be ignored.
```
--------------------------------
### RPC Best Practices: JSON Usage
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Illustrates the recommended practice of using JSON for request and response bodies in RPC communication, showing examples for both server-side Java and client-side JavaScript.
```APIDOC
## RPC Best Practices
### 1. Use JSON for Request/Response Bodies
JSON is easy to parse in both Java and JavaScript and provides a structured format for your data.
```java
// Server-side
JsonObject json = new Gson().fromJson(request.getBody(), JsonObject.class);
// Client-side
fetch(`https://${window.resourceName}/endpoint`, {
method: 'POST',
body: JSON.stringify(data)
});
```
```
--------------------------------
### Implementing RPC Error Handling
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Provides examples for robust error handling in RPC communication. It shows how to catch exceptions on the server and send error details in the response, and how to handle potential network or data errors on the client.
```java
// Server-side
try {
// Process the request
response.respond(successResponse);
} catch (Exception e) {
JsonObject error = new JsonObject();
error.addProperty("success", false);
error.addProperty("error", "An error occurred: " + e.getMessage());
response.respond(new Gson().toJson(error));
}
```
```javascript
// Client-side
try {
const response = await fetch(`https://${window.resourceName}/endpoint`, {
...
});
const data = await response.json();
if (!data.success) {
displayError(data.error);
return;
}
// Process successful response
} catch (error) {
displayError("Network error or server unavailable");
console.error(error);
}
```
--------------------------------
### Initialize Shop Application
Source: https://github.com/feathermc/feather-server-api/blob/main/examples/shop-html/src/shop.html
Sets up the shop by rendering the application's HTML, caching elements, rendering search controls, and setting up event listeners. It also subscribes to balance updates to refresh the UI.
```javascript
function init() {
renderApp();
UI.cacheElements();
UI.renderSearchControls();
setupEventListeners();
applyProportionalScaling();
state.subscribe('balance', (newBalance, oldBalance) => {
UI.updateBalance(newBalance, oldBalance);
updateBuyButtonStates();
});
}
```
--------------------------------
### Get Enabled Mods
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/mods.md
Retrieve a list of all mods currently enabled for a player asynchronously.
```java
CompletableFuture> enabledModsFuture = player.getEnabledMods();
enabledModsFuture.thenAccept(enabledMods -> {
for (FeatherMod mod : enabledMods) {
// Process each enabled mod
System.out.println("Enabled mod: " + mod.getName());
}
});
```
--------------------------------
### Initialize and Render Server Shop UI
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
Sets up the main application state, initializes DOM elements, and renders the initial shop interface. It also sets up event listeners for user interactions and server messages.
```javascript
// Main application script
(function() {
// State management
const state = {
categories: [],
currentCategory: null,
currentItems: [],
searchResults: null,
balance: 0,
loading: false
};
// DOM elements
let shopApp, notification;
// Initialize the application
function init() {
renderApp();
setupEventListeners();
}
// Create and render the main application
function renderApp() {
shopApp = document.getElementById('shop-app');
const shopHTML = `
Loading...
Server Shop
Balance: $0.00
Select a category to view items
`;
shopApp.innerHTML = shopHTML;
notification = document.getElementById('notification');
}
// Set up event listeners
function setupEventListeners() {
// Search input
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
const query = searchInput.value.trim();
if (query) {
searchItems(query);
}
}
});
// Listen for messages from the server
window.addEventListener('message', function(event) {
handleServerMessage(event.data);
});
}
// Handle messages from the server
function handleServerMessage(data) {
if (!data || typeof data !== 'object') return;
switch (data.type) {
case 'shopData':
updateShopData(data);
break;
case 'updateBalance':
updateBalance(data.balance);
break;
default:
console.log('Unknown message type:', data.type);
break;
}
}
// Update shop data from server
function updateShopData(data) {
state.balance = data.balance || 0;
state.categories = data.categories || [];
updateBalance(state.balance);
renderCategories();
}
// Update player balance
function updateBalance(balance) {
state.balance = balance;
const balanceElement = document.getElementById('player-balance');
balanceElement.textContent = `Balance: $${balance.toFixed(2)}`;
}
// Render category sidebar
function renderCategories() {
const sidebarElement = document.getElementById('categories-sidebar');
if (!state.categories || state.categories.length === 0) {
sidebarElement.innerHTML = '
`;
});
sidebarElement.innerHTML = categoriesHTML;
// Add click handlers to categories
document.querySelectorAll('.category-item').forEach(item => {
item.addEventListener('click', function() {
const categoryId = this.getAttribute('data-category-id');
loadCategory(categoryId);
});
});
}
// Load items for a category
function loadCategory(categoryId) {
state.loading = true;
state.searchResults = null;
// Find category in state
const category = state.categories.find(cat => cat.id === categoryId);
if (!category) return;
state.currentCategory = category;
// Update active state in sidebar
document.querySelectorAll('.category-item').forEach(item => {
if (item.getAttribute('data-category-id') === categoryId) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// Show loading state
const itemsContainer = document.getElementById('items-container');
itemsContainer.innerHTML = ' Loading items...';
```
--------------------------------
### Get Blocked Mods
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/mods.md
Retrieve a list of all mods currently blocked for a player asynchronously.
```java
CompletableFuture> blockedModsFuture = player.getBlockedMods();
blockedModsFuture.thenAccept(blockedMods -> {
for (FeatherMod mod : blockedMods) {
// Process each blocked mod
System.out.println("Blocked mod: " + mod.getName());
}
});
```
--------------------------------
### Deploy Website (SSH)
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/README.md
Builds and deploys the website using SSH. Assumes SSH keys are configured.
```bash
$ USE_SSH=true yarn deploy
```
--------------------------------
### Get WaypointService Instance
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/waypoints.md
Access the WaypointService to manage player waypoints. This is the main interface for all waypoint-related operations.
```java
WaypointService waypointService = FeatherAPI.getWaypointService();
```
--------------------------------
### Register and Display a Basic UI Page
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/overview.md
Register a UI page with a URL, set up lifecycle handlers, create the page for a player, and display it. Use `openPageForPlayer` to give the page input focus.
```java
UIPage page = FeatherAPI.getUIService().registerPage(this, "https://example.com/ui.html");
page.setLifecycleHandler(new UILifecycleHandlerAdapter() {
@Override
public void onCreated(FeatherPlayer player) {
// The page has been created for the player
// You can send initial data here
}
@Override
public void onDestroyed(FeatherPlayer player) {
// The page has been destroyed for the player
// Clean up any resources here
}
});
FeatherPlayer player = FeatherAPI.getPlayerService().getPlayer(playerUUID);
FeatherAPI.getUIService().createPageForPlayer(player, page);
FeatherAPI.getUIService().showOverlayForPlayer(player, page);
FeatherAPI.getUIService().openPageForPlayer(player, page);
```
--------------------------------
### Build Discord Activity with Timestamps
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/rich-presence.md
Adds start and end timestamps to a DiscordActivity, useful for showing activity duration or countdowns. Timestamps should be in milliseconds.
```java
long now = System.currentTimeMillis();
long endTime = now + (30 * 60 * 1000); // 30 minutes from now
DiscordActivity.builder()
.withStartTimestamp(now)
.withEndTimestamp(endTime)
.build();
```
--------------------------------
### Create Server List Background from File Path
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
This snippet demonstrates creating a ServerListBackground object from a specified file path. It's a common step before setting the background.
```java
Path imagePath = plugin.getDataFolder().toPath().resolve("backgrounds/my-background.png");
ServerListBackground background = metaService.getServerListBackgroundFactory().byPath(imagePath);
```
--------------------------------
### Build Static Website
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/README.md
Generates the static website content into the 'build' directory for hosting.
```bash
$ yarn build
```
--------------------------------
### Get Player Item Quantity Utility
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
Calculates the total quantity of a specific item a player has in their inventory. It handles cases where the player is offline or the item material is invalid.
```java
private int getPlayerItemQuantity(FeatherPlayer featherPlayer, ShopItem item) {
Player player = Bukkit.getPlayer(featherPlayer.getUniqueId());
if (player == null) return 0;
int count = 0;
Material material = Material.getMaterial(item.getMaterial());
if (material == null) return 0;
for (ItemStack stack : player.getInventory().getContents()) {
if (stack != null && stack.getType() == material) {
count += stack.getAmount();
}
}
return count;
}
```
--------------------------------
### Main Shop Plugin Class
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
This class serves as the main entry point for the shop plugin. It handles initialization, loading shop items from configuration, creating the UI page, and registering event listeners and commands. Use this as a template for your own UI plugins.
```java
public class ShopPlugin extends JavaPlugin {
private UIPage shopPage;
private ShopManager shopManager;
private EconomyProvider economyProvider;
@Override
public void onEnable() {
// Initialize dependencies
this.shopManager = new ShopManager();
this.economyProvider = new VaultEconomyProvider();
// Load shop items
loadShopItems();
// Create the UI page
createShopUI();
// Register event listeners
FeatherAPI.getEventService().subscribe(PlayerHelloEvent.class, this::onPlayerHello, this);
// Register commands
getCommand("shop").setExecutor(new ShopCommand(this));
}
private void loadShopItems() {
// Load shop categories and items from config
ConfigurationSection categoriesSection = getConfig().getConfigurationSection("categories");
if (categoriesSection == null) {
saveDefaultConfig();
categoriesSection = getConfig().getConfigurationSection("categories");
}
for (String categoryKey : categoriesSection.getKeys(false)) {
String categoryName = categoriesSection.getString(categoryKey + ".name");
String categoryIcon = categoriesSection.getString(categoryKey + ".icon");
ShopCategory category = new ShopCategory(categoryKey, categoryName, categoryIcon);
ConfigurationSection itemsSection =
categoriesSection.getConfigurationSection(categoryKey + ".items");
if (itemsSection != null) {
for (String itemKey : itemsSection.getKeys(false)) {
String itemName = itemsSection.getString(itemKey + ".name");
String itemMaterial = itemsSection.getString(itemKey + ".material");
double buyPrice = itemsSection.getDouble(itemKey + ".buy-price", -1);
double sellPrice = itemsSection.getDouble(itemKey + ".sell-price", -1);
ShopItem item = new ShopItem(itemKey, itemName, itemMaterial, buyPrice, sellPrice);
category.addItem(item);
}
}
this.shopManager.addCategory(category);
}
}
private void createShopUI() {
// Load HTML content from resources
String htmlContent;
try {
htmlContent = loadResource("shop.html");
} catch (IOException e) {
getLogger().severe("Failed to load shop UI HTML");
e.printStackTrace();
return;
}
// Create data URL
String dataUrl =
"data:text/html;base64," + Base64.getEncoder().encodeToString(htmlContent.getBytes());
// Register UI page
this.shopPage = FeatherAPI.getUIService().registerPage(this, dataUrl);
// Set up handlers
this.shopPage.setLifecycleHandler(new ShopLifecycleHandler(this));
this.shopPage.setFocusHandler(new ShopFocusHandler(this));
this.shopPage.setVisibilityHandler(new ShopVisibilityHandler(this));
// Register RPC controller
FeatherAPI.getUIService().registerCallbacks(this.shopPage, new ShopController(this));
}
private String loadResource(String resourceName) throws IOException {
try (InputStream is = getClassLoader().getResourceAsStream(resourceName)) {
if (is == null) {
throw new IOException("Resource not found: " + resourceName);
}
return new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8);
}
}
public void openShopForPlayer(FeatherPlayer player) {
// Create the page if it doesn't exist for this player
FeatherAPI.getUIService().createPageForPlayer(player, this.shopPage);
// Show the page and give it focus
FeatherAPI.getUIService().openPageForPlayer(player, this.shopPage);
// Send initial shop data
sendShopData(player);
}
private void sendShopData(FeatherPlayer player) {
// Create shop data message
JsonObject message = new JsonObject();
message.addProperty("type", "shopData");
// Add player balance
message.addProperty("balance", this.economyProvider.getBalance(player));
// Add categories
JsonArray categoriesArray = new JsonArray();
for (ShopCategory category : this.shopManager.getCategories()) {
JsonObject categoryObj = new JsonObject();
categoryObj.addProperty("id", category.getId());
categoryObj.addProperty("name", category.getName());
categoryObj.addProperty("icon", category.getIcon());
categoriesArray.add(categoryObj);
}
message.add("categories", categoriesArray);
// Send message
FeatherAPI.getUIService().sendPageMessage(player, this.shopPage, new Gson().toJson(message));
}
private void onPlayerHello(PlayerHelloEvent event) {
// Optionally open shop automatically when player joins
// or just wait for them to use the /shop command
}
public ShopManager getShopManager() {
return this.shopManager;
}
public EconomyProvider getEconomyProvider() {
return this.economyProvider;
}
public UIPage getShopPage() {
return this.shopPage;
}
}
```
--------------------------------
### Set Server List Background from File
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
Use this to set a server list background from an image file. Ensure the image meets Feather's requirements for format, dimensions, and file size. Handles potential exceptions during the process.
```java
try {
MetaService metaService = FeatherAPI.getMetaService();
ServerListBackground background = metaService.getServerListBackgroundFactory()
.byPath(plugin.getDataFolder().toPath().resolve("server-background.png"));
metaService.setServerListBackground(background);
} catch (ServerListBackgroundException | IOException e) {
e.printStackTrace();
}
```
--------------------------------
### Update Discord Activity on Player Join
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/rich-presence.md
Subscribes to PlayerHelloEvent to automatically update a player's Discord activity upon joining the server. Sets initial status and start timestamp.
```java
FeatherAPI.getEventService().subscribe(PlayerHelloEvent.class, event -> {
FeatherPlayer player = event.getPlayer();
DiscordActivity activity = DiscordActivity.builder()
.withImage("https://example.com/server-icon.png")
.withImageText("Epic Survival Server")
.withState("Just joined the server")
.withDetails("Playing on Epic Survival")
.withStartTimestamp(System.currentTimeMillis())
.build();
FeatherAPI.getMetaService().updateDiscordActivity(player, activity);
});
```
--------------------------------
### Implement RPC Rate Limiting
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Protects the server from spam by limiting the rate of RPC calls from individual players. This example uses a HashMap to track the last request time for each player.
```java
private final Map lastRequestTime = new HashMap<>();
private static final long MIN_REQUEST_INTERVAL = 500; // milliseconds
@RpcHandler("action")
public void handleAction(RpcRequest request, RpcResponse response) {
FeatherPlayer player = request.getSource();
UUID playerId = player.getUniqueId();
// Check if the player is making requests too quickly
long currentTime = System.currentTimeMillis();
Long lastRequest = lastRequestTime.get(playerId);
if (lastRequest != null && currentTime - lastRequest < MIN_REQUEST_INTERVAL) {
// Request is too soon after the previous one
JsonObject error = new JsonObject();
error.addProperty("success", false);
error.addProperty("error", "Please wait before making another request");
response.respond(new Gson().toJson(error));
return;
}
// Update the last request time
lastRequestTime.put(playerId, currentTime);
// Process the request normally
// ...
}
```
--------------------------------
### Set Up Core Event Listeners
Source: https://github.com/feathermc/feather-server-api/blob/main/examples/shop-html/src/shop.html
Attaches various event listeners to the window and document for handling resize, keydown (Escape), visibility changes, mouse wheel events, and incoming messages. Also sets up click listeners for notifications.
```javascript
function setupEventListeners() {
const resizeHandler = debounce(applyProportionalScaling, 200);
window.addEventListener('resize', resizeHandler);
const escKeyHandler = function(event) {
if (event.key === 'Escape' && !state.modalOpen) {
closeShop();
}
};
document.addEventListener('keydown', escKeyHandler);
const visibilityHandler = function() {
if (document.visibilityState === 'hidden') {
closeShop();
}
};
document.addEventListener('visibilitychange', visibilityHandler);
const wheelHandler = function(event) {
const target = event.target;
let scrollableParent = findScrollableParent(target);
if (scrollableParent) {
const { scrollTop, scrollHeight, clientHeight } = scrollableParent;
if ( (event.deltaY < 0 && scrollTop <= 0) || (event.deltaY > 0 && scrollTop + clientHeight >= scrollHeight) ) {
event.preventDefault();
}
} else {
event.preventDefault();
}
};
document.addEventListener('wheel', wheelHandler, { passive: false });
const messageHandler = function(event) {
handleServerMessage(event.data);
};
window.addEventListener('message', messageHandler);
const notificationClickHandler = function(event) {
if (event.target.classList.contains('notification-close')) {
const notification = event.target.closest('.notification');
if (notification) {
removeNotification(notification.id);
}
return;
}
const notification = event.target.closest('.notification');
if (notification) {
const notificationId = notification.id;
if (state.activeNotifications[notificationId] && !state.activeNotifications[notificationId].loading) {
removeNotification(notificationId);
}
}
};
DOM.notificationContainer.addEventListener('click', notificationClickHandler);
}
```
--------------------------------
### Create Server List Background from Bytes
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
Use this method when you have image data as a byte array. You can specify the image format explicitly or rely on automatic detection if supported.
```java
// With explicit format
ServerListBackground background = factory.fromBytes(imageBytes, ImageFormat.PNG);
// With automatic format detection (if supported by the implementation)
ServerListBackground background = factory.fromBytes(imageBytes);
```
--------------------------------
### Update Discord Activity for Minigame Entry
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/rich-presence.md
Dynamically updates a player's Discord activity when they enter a minigame. Includes game-specific image, state, details, party size, and start timestamp.
```java
public void onPlayerJoinMinigame(FeatherPlayer player, String gameName, int playerCount, int maxPlayers) {
DiscordActivity activity = DiscordActivity.builder()
.withImage("https://example.com/minigames/" + gameName.toLowerCase() + ".png")
.withImageText(gameName)
.withState("Playing " + gameName)
.withDetails("Round starting soon")
.withPartySize(playerCount, maxPlayers)
.withStartTimestamp(System.currentTimeMillis())
.build();
FeatherAPI.getMetaService().updateDiscordActivity(player, activity);
}
```
--------------------------------
### Get Shop Category
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
Retrieves a shop category and its associated items. This RPC call allows the client to fetch details about a specific category, including item information, pricing, and player-specific quantities.
```APIDOC
## RPC getCategory
### Description
Retrieves a shop category and its associated items. This RPC call allows the client to fetch details about a specific category, including item information, pricing, and player-specific quantities.
### Method
RPC
### Endpoint
N/A (RPC)
### Parameters
#### Request Body
- **categoryId** (string) - Required - The ID of the category to retrieve.
### Request Example
```json
{
"categoryId": "example_category_id"
}
```
### Response
#### Success Response
- **categoryId** (string) - The ID of the category.
- **categoryName** (string) - The name of the category.
- **items** (array) - A list of items in the category.
- **id** (string) - The ID of the item.
- **name** (string) - The name of the item.
- **material** (string) - The material of the item.
- **buyPrice** (double) - Optional - The price to buy the item.
- **sellPrice** (double) - Optional - The price to sell the item.
- **playerQuantity** (integer) - The quantity of this item the player possesses.
#### Response Example
```json
{
"categoryId": "weapons",
"categoryName": "Weapons",
"items": [
{
"id": "iron_sword",
"name": "Iron Sword",
"material": "IRON_SWORD",
"buyPrice": 100.0,
"sellPrice": 50.0,
"playerQuantity": 5
}
]
}
```
#### Error Response
- **error** (string) - Description of the error (e.g., "Category not found", "Invalid request format").
```
--------------------------------
### Deploy Website (No SSH)
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/README.md
Builds and deploys the website without SSH. Requires specifying your GitHub username.
```bash
$ GIT_USER= yarn deploy
```
--------------------------------
### Register and Manage Web UI Pages with UIService
Source: https://context7.com/feathermc/feather-server-api/llms.txt
This Java code demonstrates how to register a UI page, handle its lifecycle events (creation, destruction, show, hide, focus gain/loss, load errors), and manage its visibility and focus for players. It also shows how to send JSON messages to the client-side UI.
```java
public class ShopPlugin extends JavaPlugin {
private UIPage shopPage;
@Override
public void onEnable() {
// Load HTML from plugin resources and encode as a data URL
String html = loadResource("shop.html");
String dataUrl = "data:text/html;base64," +
Base64.getEncoder().encodeToString(html.getBytes(StandardCharsets.UTF_8));
// Register the page (one per plugin)
shopPage = FeatherAPI.getUIService().registerPage(this, dataUrl);
// Lifecycle: creation / destruction
shopPage.setLifecycleHandler(new UILifecycleHandlerAdapter() {
@Override public void onCreated(FeatherPlayer p) { sendInitialData(p); }
@Override public void onDestroyed(FeatherPlayer p) { /* cleanup */ }
});
// Visibility: shown / hidden
shopPage.setVisibilityHandler(new UIVisibilityHandlerAdapter() {
@Override public void onShow(FeatherPlayer p) { sendInitialData(p); }
@Override public void onHide(FeatherPlayer p) { /* pause updates */ }
});
// Focus: keyboard/mouse input gained / lost
shopPage.setFocusHandler(new UIFocusHandlerAdapter() {
@Override public void onFocusGained(FeatherPlayer p) { refreshBalance(p); }
@Override public void onFocusLost(FeatherPlayer p) { /* save state */ }
});
// Load errors
shopPage.setLoadHandler(new UILoadHandlerAdapter() {
@Override public void onLoadError(FeatherPlayer p, String err) {
getLogger().warning("UI load error for " + p.getName() + ": " + err);
}
});
// Register RPC controller
FeatherAPI.getUIService().registerCallbacks(shopPage, new ShopController(this));
}
public void openShop(FeatherPlayer fp) {
UIService ui = FeatherAPI.getUIService();
ui.createPageForPlayer(fp, shopPage); // instantiate for player
ui.openPageForPlayer(fp, shopPage); // show + give input focus
}
public void closeShop(FeatherPlayer fp) {
UIService ui = FeatherAPI.getUIService();
ui.closePageForPlayer(fp, shopPage); // remove focus (hides if not pinned)
ui.destroyPageForPlayer(fp, shopPage); // fully remove
}
private void sendInitialData(FeatherPlayer fp) {
JsonObject msg = new JsonObject();
msg.addProperty("type", "shopData");
msg.addProperty("balance", getBalance(fp));
FeatherAPI.getUIService().sendPageMessage(fp, shopPage, new Gson().toJson(msg));
}
}
```
--------------------------------
### Making RPC Calls from Client-Side JavaScript
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-rpc-system.md
Demonstrates how to initiate an RPC call from the client's JavaScript code using the fetch API. It includes sending POST requests with JSON bodies and handling the JSON response.
```javascript
async function buyItem(itemId, quantity) {
try {
const response = await fetch(`https://${window.resourceName}/buyItem`, {
method: 'POST',
body: JSON.stringify({
itemId: itemId,
quantity: quantity
})
});
const result = await response.json();
if (result.success) {
showSuccessMessage(`Successfully purchased ${quantity} items!`);
} else {
showErrorMessage(result.error || 'Failed to purchase items');
}
} catch (error) {
showErrorMessage('Network error, please try again');
console.error(error);
}
}
// Call the function when a button is clicked
document.getElementById('buy-button').addEventListener('click', () => {
const itemId = parseInt(document.getElementById('item-id').value);
const quantity = parseInt(document.getElementById('quantity').value);
buyItem(itemId, quantity);
});
```
--------------------------------
### Accessing FeatherAPI Services
Source: https://context7.com/feathermc/feather-server-api/llms.txt
Demonstrates how to access the five core services provided by the FeatherAPI static facade.
```APIDOC
## Accessing FeatherAPI Services
`FeatherAPI` is a static facade that provides access to all five services. All service accessors throw if the backing plugin has not been loaded yet.
```java
// Accessing every service through FeatherAPI
EventService eventService = FeatherAPI.getEventService();
PlayerService playerService = FeatherAPI.getPlayerService();
UIService uiService = FeatherAPI.getUIService();
WaypointService waypointService = FeatherAPI.getWaypointService();
MetaService metaService = FeatherAPI.getMetaService();
```
```
--------------------------------
### Image Loading Thread Safety
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/meta/server-banner.md
Perform image loading operations asynchronously to prevent blocking the main server thread. This example uses Bukkit's scheduler to run the task off the main thread.
```java
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
ServerListBackground background = FeatherAPI.getMetaService()
.getServerListBackgroundFactory()
.byPath(imagePath);
try {
FeatherAPI.getMetaService().setServerListBackground(background);
logger.info("Server background updated successfully");
} catch (Exception e) {
logger.error("Error setting server background", e);
}
} catch (Exception e) {
logger.error("Error loading server background", e);
}
});
```
--------------------------------
### Initialize Shop on DOM Load
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
Attaches an event listener to initialize the shop interface once the HTML document has been fully loaded and parsed.
```javascript
document.addEventListener('DOMContentLoaded', init);
```
--------------------------------
### Implement Shop RPC Controller
Source: https://github.com/feathermc/feather-server-api/blob/main/docs/docs/server-api/ui/ui-comprehensive-example.md
This Java code implements an RPC controller for a shop system. It handles requests for 'getCategory', 'buyItem', and 'sellItem', interacting with shop managers, economy providers, and player data.
```java
public class ShopController implements RpcController {
private final ShopPlugin plugin;
public ShopController(ShopPlugin plugin) {
this.plugin = plugin;
}
@RpcHandler("getCategory")
public void getCategory(RpcRequest request, RpcResponse response) {
FeatherPlayer player = request.getSource();
try {
JsonObject requestData = new Gson().fromJson(request.getBody(), JsonObject.class);
String categoryId = requestData.get("categoryId").getAsString();
ShopCategory category = this.plugin.getShopManager().getCategory(categoryId);
if (category == null) {
sendErrorResponse(response, "Category not found");
return;
}
// Create response with category items
JsonObject responseData = new JsonObject();
responseData.addProperty("categoryId", category.getId());
responseData.addProperty("categoryName", category.getName());
JsonArray itemsArray = new JsonArray();
for (ShopItem item : category.getItems()) {
JsonObject itemObj = new JsonObject();
itemObj.addProperty("id", item.getId());
itemObj.addProperty("name", item.getName());
itemObj.addProperty("material", item.getMaterial());
if (item.canBuy()) {
itemObj.addProperty("buyPrice", item.getBuyPrice());
}
if (item.canSell()) {
itemObj.addProperty("sellPrice", item.getSellPrice());
}
// Check if player has this item and how many
int playerQuantity = getPlayerItemQuantity(player, item);
itemObj.addProperty("playerQuantity", playerQuantity);
itemsArray.add(itemObj);
}
responseData.add("items", itemsArray);
response.respond(new Gson().toJson(responseData));
} catch (Exception e) {
this.plugin.getLogger().warning("Error processing getCategory request: " + e.getMessage());
sendErrorResponse(response, "Invalid request format");
}
}
@RpcHandler("buyItem")
public void buyItem(RpcRequest request, RpcResponse response) {
FeatherPlayer player = request.getSource();
try {
JsonObject requestData = new Gson().fromJson(request.getBody(), JsonObject.class);
String categoryId = requestData.get("categoryId").getAsString();
String itemId = requestData.get("itemId").getAsString();
int quantity = requestData.get("quantity").getAsInt();
if (quantity <= 0) {
sendErrorResponse(response, "Invalid quantity");
return;
}
// Get the item
ShopItem item = this.plugin.getShopManager().getItem(categoryId, itemId);
if (item == null) {
sendErrorResponse(response, "Item not found");
return;
}
if (!item.canBuy()) {
sendErrorResponse(response, "This item cannot be purchased");
return;
}
// Calculate total cost
double totalCost = item.getBuyPrice() * quantity;
// Check player balance
double playerBalance = this.plugin.getEconomyProvider().getBalance(player);
if (playerBalance < totalCost) {
sendErrorResponse(response, "Insufficient funds");
return;
}
// Process purchase
boolean success = processPurchase(player, item, quantity, totalCost);
if (!success) {
sendErrorResponse(response, "Failed to process purchase");
return;
}
// Send success response
JsonObject responseData = new JsonObject();
responseData.addProperty("success", true);
responseData.addProperty("newBalance", this.plugin.getEconomyProvider().getBalance(player));
responseData.addProperty(
"message", "Successfully purchased " + quantity + " " + item.getName());
response.respond(new Gson().toJson(responseData));
} catch (Exception e) {
this.plugin.getLogger().warning("Error processing buyItem request: " + e.getMessage());
sendErrorResponse(response, "Invalid request format");
}
}
@RpcHandler("sellItem")
public void sellItem(RpcRequest request, RpcResponse response) {
FeatherPlayer player = request.getSource();
try {
JsonObject requestData = new Gson().fromJson(request.getBody(), JsonObject.class);
String categoryId = requestData.get("categoryId").getAsString();
String itemId = requestData.get("itemId").getAsString();
int quantity = requestData.get("quantity").getAsInt();
if (quantity <= 0) {
sendErrorResponse(response, "Invalid quantity");
return;
}
// Get the item
ShopItem item = this.plugin.getShopManager().getItem(categoryId, itemId);
if (item == null) {
sendErrorResponse(response, "Item not found");
return;
}
if (!item.canSell()) {
sendErrorResponse(response, "This item cannot be sold");
return;
}
// Check if player has enough items
int playerQuantity = getPlayerItemQuantity(player, item);
```