Server and Java
- Java 17 or newer for building and running Graaly.
- A plugin-capable server from 1.7.10 through 26.2.
- The legacy-launcher agent from the release only when an old launcher rejects your modern JVM.
JavaScript, TypeScript, and Python use one language-native API from Minecraft 1.7.10 through 26.2. C uses a sandboxed low-level ABI designed to teach structs, pointers, ownership, callbacks, and memory while Graaly keeps host objects behind checked handles.
Start here before copying an example. Graaly itself runs on Java 17 or newer; choose a JVM version that also satisfies your server release. Node.js, Python, Zig, and Clang are development tools, not separate in-server runtimes.
.mjs entry file.graaly or @graaly/react.async/await.Player struct.DEADBEEF red zones, overflow and double-free diagnostics, and a contained memory-trap lab.Download Graaly-1.0.0.jar and, only for historical launchers, the matching legacy-launcher agent.
Place only the runtime JAR in plugins/; keep server and PacketEvents JARs separate.
Start once. Graaly downloads the enabled runtimes to plugins/Graaly/runtime/25.2.4/, then loads bundles from plugins/Graaly/scripts/.
GraalJS, GraalPy, and GraalWasm are not inside the plugin JAR. Graaly downloads only the enabled languages from Maven Central, checks exact size and SHA-256, and reuses the verified cache on every restart.
auto-download to false.runtime:
auto-download: true
languages:
javascript: true
python: true
c: true
connect-timeout-seconds: 20
request-timeout-seconds: 180
retry-attempts: 2Your public imports and autocomplete stay fixed from 1.7.10 to 26.2. Version-specific names, signatures, and fallbacks live behind Graaly's adapter boundary.
Materials.GRASS_BLOCKcontract 1.0faithful native valueThe same imports, modules, properties, and canonical constants stay in autocomplete on every supported release.
Graaly maps historical names and method shapes inside its adapter. Plugin code never selects a server version.
If an old release cannot represent a feature, Graaly reports it through compatibility and GraalyUnsupportedFeature.
import { Attributes, Materials, compatibility } from "graaly";
const floor = Materials.GRASS_BLOCK; // same name everywhere
if (compatibility.supports("attributes")) {
const health = Attributes.MAX_HEALTH;
}from graaly import Attributes, Materials, compatibility
floor = Materials.GRASS_BLOCK # same name everywhere
if compatibility.supports("attributes"):
health = Attributes.MAX_HEALTHoff_handavailable from 1.9attributesavailable from 1.9entity_aiavailable from 1.9entity_invulnerableavailable from 1.9glowingavailable from 1.9elytraavailable from 1.9entity_gravityavailable from 1.10persistent_dataavailable from 1.14custom_model_dataavailable from 1.14allayavailable from 1.19display_entitiesavailable from 1.19.4data_componentsavailable from 1.20.5bundlesavailable from 1.21.2dialogsavailable from 1.21.6Install Graaly once, create one plugin bundle, copy it to the documented server directory, and verify that the loader discovered it. Every path below is executable, not just illustrative.
Graaly does not currently publish a release JAR. Build the current source, copy the single runtime JAR into your server's plugins/ directory, and start the server once.
Java 17+ and Maven produce runtime/target/Graaly-1.0.0.jar.
Copy only that JAR to server/plugins/. The legacy agent is not a plugin.
Graaly creates its directory and downloads the enabled GraalJS/GraalPy runtimes.
Check the ready log, then run graaly status from the console.
git clone https://github.com/sk8erboi17/Graaly.git
cd Graaly/runtime
mvn clean package
# Copy the runtime, not the legacy agent, into the server:
cp target/Graaly-1.0.0.jar /absolute/path/to/server/plugins/
cd /absolute/path/to/server
java -jar server.jar noguiserver/
├── server.jar
└── plugins/
├── Graaly-1.0.0.jar
└── Graaly/
├── config.yml
├── runtime/25.2.4/
└── scripts/[Graaly] Graaly is ready: 0 guest plugin(s), downloaded Graal …In the server console run graaly status. In game use /graaly status as an operator or with graaly.admin permission.
The selected tab changes the source, manifest, build command, destination directory, and load instructions together.
name: WelcomeTS
version: 1.0.0
main: dist/main.mjs
commands:
hello:
description: Say helloimport { commands, events, PlayerJoinEvent }
from "graaly";
events.on(PlayerJoinEvent, event => {
event.joinMessage = `§b${event.player.name} joined`;
event.player.sendMessage("&bWelcome!");
});
commands.on("hello", context => {
context.reply(`&bHello ${context.sender.name}`);
return true;
});plugin.wasm. Java deploys a compiled JAR.The whole bundle is one directory. Its suffix tells Graaly which language loader to use, and plugin.yml points to the entry file inside it.
Run the language toolchain locally; Node.js, CPython, Zig, and Clang are not required on the game server.
Place the bundle under plugins/Graaly/scripts/.
Follow the load rule below, check the log, then execute /hello in game.
cd /absolute/path/to/Graaly/runtime/examples/TypeScriptHello.jsplugin
npm ci
npm run build
# Output: dist/main.mjsmkdir -p /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/dist
cp plugin.yml /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/
cp dist/main.mjs /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/dist/plugins/Graaly/scripts/WelcomeTS.jsplugin/
├── plugin.yml
└── dist/
└── main.mjsFor a new bundle, restart the server. After changing code in an already loaded bundle, rebuild, copy dist/main.mjs, then run graaly reload in the server console or /graaly reload in game.
plugin.yml change: restart the server. Source-only change to an already loaded bundle: copy the rebuilt file, then use /graaly reload as an operator.Graaly gives ordinary language constructs useful game-server data. Follow one path at a time, copy the complete example, then recognize what belongs to the language, what belongs to Graaly, and what only exists in a browser or Node.js environment.
Control flow, types, collections, async syntax, or C pointers, bits, layout, and ownership.
Players, worlds, events, commands, tasks, entities, and packets.
Live state on Java 17 or newer. This is not a web page or a Node.js process.
Graaly runs server-side, so browser globals such as window, document, and HTMLElement are intentionally absent there. Website boards are still experimental and remain outside the stable public contract until their browser lifecycle and input behavior are ready.
Import explicit Graaly names and alias modules only when it improves clarity.
from graaly import players
def online_names() -> list[str]:
return [player.name for player in players]from graaly import (
CommandContext, PlayerJoinEvent, command, event, players, tasks,
)
@event(PlayerJoinEvent)
async def welcome(event: PlayerJoinEvent) -> None:
others = [player.name for player in players
if player != event.player]
await tasks.sleep_ticks(20)
event.player.send_message(
f"Online now: {', '.join(others) or 'only you'}"
)
@command("team")
def team(context: CommandContext) -> bool:
match context.args:
case ["list"]:
context.reply(", ".join(p.name for p in players))
case ["find", name] if (target := players.exact(name)) is not None:
context.reply(f"Found {target.name}")
case _:
return False
return Truefrom collections.abc import Iterator
from contextlib import contextmanager
from graaly import Player, info, players
def online_names() -> Iterator[str]:
for player in players:
yield player.name
@contextmanager
def temporary_health(player: Player, value: float) -> Iterator[Player]:
previous = player.health
try:
if value < 0:
raise ValueError("health cannot be negative")
player.health = min(value, player.max_health)
yield player
except ValueError as error:
info(f"Invalid health: {error}")
raise
finally:
player.health = previous
def show_temporary_health(player: Player) -> None:
with temporary_health(player, 20.0) as healed:
healed.send_message(", ".join(online_names()))info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.Ordinary C data and function pointers live in the plugin's WebAssembly linear memory.
Host objects never become native C pointers. Graaly validates handles and every host memory range.
A WebAssembly memory trap quarantines the guest instance instead of exposing arbitrary JVM memory.
Choose arrays and scalar fields yourself. A snapshot is copied C data, not a live Java object.
Pass &player, receive Player *, write through ->, and size buffers with sizeof.
Event and command callbacks stay inside the Wasm module. Java receives only callback IDs.
The optional teaching heap detects red-zone overflow, invalid free, double free, and poisons logical frees with DEADBEEF.
#include <graaly/graaly.h>
#include <string.h>
typedef struct Player {
graaly_player_t host;
char name[32];
double health;
int level;
} Player;
static bool snapshot(graaly_player_t player, Player *out) {
if (out == NULL || !graaly_is_valid(player)) return false;
memset(out, 0, sizeof *out);
out->host = player;
size_t required = 0;
double level = 0.0;
if (graaly_player_name(player, out->name, sizeof out->name, &required) != GRAALY_OK
|| graaly_player_health(player, &out->health) != GRAALY_OK
|| graaly_player_level(player, &level) != GRAALY_OK) return false;
out->level = (int) level;
return true;
}graaly_player_t player = {0};
if (graaly_player_find_exact("Steve", &player) != GRAALY_OK) return;
double health = 0.0;
bool allowed = false;
graaly_player_health(player, &health);
graaly_player_has_permission(player, "example.use", &allowed);
if (health < 10.0) {
graaly_player_health_write(player, 20.0);
}
graaly_location_t location = {0};
graaly_player_location(player, &location);
graaly_release(&location);
graaly_release(&player);uint32_t *value = graaly_debug_malloc(sizeof *value);
if (value == NULL) return;
*value = UINT32_C(0x12345678);
graaly_memory_status_t before = graaly_debug_check(value);
graaly_memory_status_t freed = graaly_debug_free(value);
/* logical free: storage is quarantined for the lesson */
uint32_t poison = *value; /* 0xDEADBEEF */
graaly_memory_status_t twice = graaly_debug_free(value);
/* twice == GRAALY_MEMORY_ALREADY_FREED */graaly_player_t players[64];
size_t count = 0;
graaly_player_list(players, 64, &count);
graaly_http_request_t request = {0};
graaly_http_get(url, "{}", 15000, on_http, &request);
graaly_ui_render(player, snapshot_json, on_ui_action);
graaly_packet_binding_t binding = {0};
graaly_packet_on_receive(
packet_type, GRAALY_PRIORITY_NORMAL, on_packet, &binding
);Run /cbits, /cunion, and /clayout in EducationalC. Each displayed function is compiled for wasm32-wasi and tested with memory and undefined-behavior sanitizers on the host.
Set, clear, toggle, and extract unsigned flags. Cast bytes before shifting; reject shift counts outside 0..31 for uint32_t.
#include <graaly/bits.h>
#include <stdlib.h>
/* Unsigned masks describe plugin-owned state, not Java object layout. */
enum { LAB_VISIBLE = 1u << 0u, LAB_SELECTED = 1u << 1u, LAB_DIRTY = 1u << 2u };
static inline uint32_t lab_flags(void) {
uint32_t flags = LAB_VISIBLE | LAB_SELECTED;
flags |= LAB_DIRTY; /* set */
flags &= ~((uint32_t) LAB_SELECTED); /* clear */
flags ^= LAB_VISIBLE; /* toggle */
return flags;
}
static inline uint32_t lab_rgb(uint8_t red, uint8_t green, uint8_t blue) {
return ((uint32_t) red << 16u) | ((uint32_t) green << 8u) | (uint32_t) blue;
}
/* graaly_u32_shift_left(1u, 31u, &out) succeeds; shift 32 is rejected. */
The bitset and byte helpers are in <graaly/bits.h>. Your structs and union tags remain ordinary plugin-owned C data.
C bit-field order, signed shifts, and struct padding depend on the language and target rules. WebAssembly containment does not make undefined C behavior valid.
EducationalC.cplugin exposes /cheap, /coverflow, and the operator-only /csegfault labs. The last command intentionally traps the guest; inspect the log, fix or review the code, then run /graaly reload.An event is a notification from the server. Pick the event that describes the moment you care about, register a listener, read its data, then optionally change or cancel the action.
A player moves, a block breaks, an entity takes damage, or a world loads.
The callback receives one typed event object. No string resolver is needed.
Read properties, change editable values, or set cancelled when supported.
265 events
A player is about to break a block, before the server removes it.
Use it for region protection, custom drops, building rules, and block mechanics. This event also lets you change expToDrop, dropItems.
block, expToDrop, cancelled, player, dropItems
expToDrop, cancelled, dropItems
blockBlockexpToDropnumbereditablecancelledbooleaneditableplayerPlayerdropItemsbooleaneditableBlockBreakEvent · @EventHandlerimport { BlockBreakEvent, events, Material } from "graaly";
events.on(BlockBreakEvent, event => {
if (event.block.type === Material.DIAMOND_ORE
&& !event.player.hasPermission("mine.diamond")) {
event.cancelled = true;
event.player.sendMessage("You cannot mine this ore.");
return;
}
event.expToDrop = 0;
});Graaly maps this event directly to its Java listener type.
Search by goal, choose a task, and see what it does before the code. Every guide includes native JS, TS, and Python plus the exact Java pattern it replaces.
10 practical guides
Use this after declaring the command name in plugin.yml.
Runs your callback with the sender, label, arguments, reply helper, and permission helper already adapted.
Command name and a callback
true when handled; false to show plugin.yml usage
commands.on(name, handler)context.senderplayers.isPlayer(sender)context.reply(message)context.hasPermission(node)JavaPlugin#getCommand(String) → PluginCommand#setExecutor(CommandExecutor)import { commands, players } from "graaly";
commands.on("graaly", context => {
if (!players.isPlayer(context.sender)) {
context.reply("&cOnly players can use this command.");
return true;
}
const player = context.sender; // narrowed to Player
if (!context.hasPermission("graaly.admin")) {
context.reply("&cYou do not have permission.");
return true;
}
player.sendMessage(`§aHello ${player.name} from Graaly`);
return true;
});Important: context.sender is always the command sender, but it is not necessarily a Player: it may be the console or a command block. players.isPlayer(context.sender) safely narrows it to Player. players.broadcast(...) only sends a message to every online player; it does not return the player who ran the command.
Find players, message them, change inventory or health, teleport, control movement, permissions, scoreboards, metadata, time, and weather. The complete inherited Player catalog remains one click away.
9 practical guides
Start here whenever a command, task, or service needs a Player object.
Returns native Player values, not handles that require unwrapping.
Optional player name
Iterable<Player>, Player, or null/None
players or players.online()players.get(name)players.exact(name)Bukkit#getOnlinePlayers / getPlayer / getPlayerExactimport { players } from "graaly";
const names = [...players].map(({ name }) => name);
for (const online of players) {
online.sendMessage(`Online as ${online.name}`);
}
const steve = players.exact("Steve");
if (steve) steve.sendMessage("Found you");Location is now a first-class guide beside blocks, chunks, time, weather, spawn rules, effects, world creation, custom generators, populators, saving, and unloading.
9 practical guides
Use it anywhere the Java API expects a Location: teleporting, spawning, effects, blocks, or distances.
Creates the native Location while your code stays idiomatic JS, TS, or Python. Coordinates must be real finite numbers and conversions may not lose integer precision.
World, x, y, z, optional yaw and pitch
Location
worlds.location(world, x, y, z, yaw, pitch)location.blocklocation.chunknew org.bukkit.Location(World, double, double, double, float, float)import { info, worlds } from "graaly";
const world = worlds.get("world");
if (world) {
const location = worlds.location(world, 12.5, 70, -4.5, 90, 0);
info(`${location.blockX}, ${location.blockY}, ${location.blockZ}`);
}info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.Search the exact action you need. The attribute guide includes all 40 canonical attributes from the current contract and tells you which releases can represent each mechanic faithfully.
9 practical guides
Use it for custom mobs, NPC-like mechanics, projectiles, and arena entities.
Graaly selects the correct native spawn path, while EntityTypes.ZOMBIE, HORSE, ITEM, and every other constant preserve their TypeScript and Python return type.
Location, EntityTypes value, and optional configuration
Precisely inferred Zombie, Horse, Item, or other entity subtype
entities.spawn(location, type, options)EntityTypes.ZOMBIEWorld#spawnEntityimport { entities, EntityTypes } from "graaly";
const zombie = entities.spawn(location, EntityTypes.ZOMBIE, {
name: "§aGraaly guardian",
nameVisible: true,
});Build small menus with familiar tags while keeping a faithful game result: inventory grids and stained-glass panels, sign-based text input, and two-choice anvil dialogs. The same compiler and action model are exposed to TypeScript, JavaScript, and Python.
div, span, button, input, and dialog. Graaly parses them on the server and creates Minecraft inventory, sign, and anvil screens; it does not send HTML to the client.div · main · section · formNative panel/container; its grid area groups child elements.
span · p · label · h1…h6Text item: content becomes the item display name and tooltip.
buttonClickable inventory item; id or data-action selects the handler.
inputClickable field that opens the native four-line sign editor.
dialog openAnvil modal; exactly two button children occupy slots 0 and 1.
hrOne horizontal stained-glass line across the current grid row.
import { commands, players, ui } from "graaly";
const menu = `
<div id="profile" aria-label="Profilo" data-rows="3">
<style>
#profile {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(3, 1fr);
background: white;
border: 1px solid black;
border-radius: 8px;
}
span { grid-column: 2 / 9; grid-row: 1; color: yellow; }
input { grid-column: 3 / 8; grid-row: 2; background: lightblue; }
button { grid-column: 4 / 7; grid-row: 3; --minecraft-material: EMERALD_BLOCK; }
</style>
<span>Configura il profilo</span>
<input id="player-name" placeholder="Scrivi sul cartello">
<button id="confirm">Conferma</button>
</div>`;
commands.on("htmlgui", context => {
if (!players.isPlayer(context.sender)) return true;
const player = context.sender;
ui.renderHtml(player, menu, { actions: {
"player-name": action => player.sendMessage(`Testo: ${action.value ?? ""}`),
confirm: () => ui.renderHtml(player, `
<dialog id="save" open aria-label="Sei sicuro?">
<button id="yes" style="background: lime">Sì</button>
<button id="no" style="background: red">No</button>
</dialog>
`, { actions: {
yes: () => player.sendMessage("Confermato"),
no: () => player.sendMessage("Annullato"),
}}),
}});
return true;
});from graaly import command, players, ui
@command("pyhtmlgui")
def open_gui(context):
if not players.is_player(context.sender):
return True
ui.render_html(
context.sender,
'<input id="name" placeholder="Scrivi sul cartello">',
css='input { background: white; }',
actions={"name": lambda action: context.reply(action.value or "")},
)
return True#include <graaly/graaly.h>
static void on_action(const char *json, size_t length) {
/* JSON is pointer + length. Decode it with a JSON parser when needed. */
graaly_log_n(GRAALY_LOG_INFO, json, length);
}
static bool open_gui(graaly_sender_t sender, int argc, const graaly_string_view_t *argv) {
(void) argc; (void) argv;
graaly_player_t player = {0};
if (graaly_sender_player(sender, &player) != GRAALY_OK) return false;
graaly_status_t status = graaly_ui_render_html(player,
"<div data-rows=3><input id=name placeholder=Name>"
"<button id=confirm>Confirm</button></div>",
"input { background: white; } button { background: lime; }", on_action);
graaly_release(&player);
return status == GRAALY_OK;
}
void graaly_on_enable(void) { graaly_commands_on("chtmlgui", open_gui); }grid-template-rows, grid-row, and grid-column map to 9 columns and 1–6 inventory rows.
Named, hex, and rgb() colors map to the nearest dye. white is WHITE_STAINED_GLASS_PANE.
border paints the perimeter; border-radius leaves the four corner slots empty for a rounded-panel shape.
color, font-weight, font-style, and text decoration become Minecraft formatting codes.
--minecraft-material, --minecraft-amount, --minecraft-durability, and --minecraft-lore select item details.
Use data-action on controls and data-close-action on a container. Scripts, DOM APIs, and inline onclick are rejected.
Follow the animated IDE from the first React root to a tested FastAPI transaction. Every lesson explains the code, how it differs from browser React, why that boundary was chosen, which simpler alternative exists, and when the additional architecture becomes justified.
Follow one architectural decision at a time. Compare game UI with web React, inspect the code, and learn why each boundary exists.
import React from "react";Your component still returns JSX and React still reconciles state. Graaly implements a renderer that turns that tree into native game UI.
The genuine React programming model transfers to web work: components, props, state, Hooks, Context, composition, and one-way data flow all keep their meaning.
A website calls react-dom createRoot on an HTMLElement and produces DOM nodes. Graaly calls createRoot(player) and produces a scoreboard, inventory, boss bar, tab list, or message for that player.
The React renderer uses explicit native host elements because there is no DOM inside the game process. For small non-React menus, the separate ui.renderHtml helper compiles semantic HTML and a limited CSS grid into the same native snapshots.
@graaly/react uses React 19 and its reconciler. Components, JSX, props, hooks, context, state, effects, composition, error handling, and third-party state libraries keep their normal meaning. The only difference is the render target: game UI instead of an HTML DOM.
Join, command, inventory click, packet, or your own rule.
Keeps the player reference and publishes safe UI actions.
State decides what each player sees; updates are reconciled.
Validates requests and persists data with SQLAlchemy.
Receives live events, owns player references, and applies UI or world changes on the safe server thread.
Owns HTTP routes, validation, authentication, database sessions, migrations, and business data.
FastAPI can be your application backend, but it cannot directly receive in-process game events or hold live player objects. The small Graaly adapter is the boundary that makes the split reliable.
Graaly uses the long service key only to exchange a trusted player identity for a short-lived JWT. Every protected request carries that bearer token, while FastAPI reloads the actor's current roles and permissions from SQL before running the endpoint. A role change therefore revokes access immediately, even when the old identity token has not expired.
POST /v1/auth/session accepts the service key and player identity.
The token identifies the player; it does not freeze a permission snapshot.
Member, moderator, and admin roles resolve to current permission keys.
require_permission(...) allows or rejects before business logic.
profile.read, shop.purchase, and realtime.connect.
Profile and realtime access plus permissions.read, without role mutation.
All permissions, including permissions.manage. Bootstrap admins cannot remove their own emergency access.
Use Graaly for rules and data, plus React for UI. No HTTP service is required.
Use decorators, asyncio, and ui.render. React is optional, not mandatory.
React/TS owns presentation; FastAPI/SQLAlchemy owns durable application data.
<Message>One-shot chat, action-bar, or title output. Give it a stable unique id.
<Inventory> + <Item>Menu slots, names, lore, amounts, click handlers, and close handlers.
Java: Inventory + inventory events<Scoreboard> + <Line>Up to 15 keyed lines. Stable IDs let Graaly update only changed rows.
Java: Scoreboard + Objective + Team<BossBar>Text and progress from 0 to 1, updated only when the values change.
Java: legacy 1.8 boss display packets<Tab>Per-player header and footer that follow normal React state.
Java: setPlayerListHeaderFooter<ChatInput>A controlled or uncontrolled text field backed by the player's next chat message, with submit, cancel, and cleanup.
React: value / defaultValue / onSubmitcreateRoot(player)Creates one isolated React root. Call unmount() when the player leaves.
ref + handleTyped Inventory, Scoreboard, BossBar, Tab, and ChatInput handles expose only dismiss, refresh, ID, kind, and current props.
React 19: ref prop + imperative APIcreatePortal(...)Targets another player root while preserving the source component's Context and logical ownership.
React: genuine cross-root portalroot.getCommits()Reads immutable native-surface diffs, durations, and history so performance and batching can be proven.
Graaly: commit inspector@graaly/react-testUses the real reconciler and act() to click slots, submit input, inspect snapshots, and assert commits.
Read each pair from left to right: the React pattern controls presentation and interaction; the FastAPI pattern protects data and business rules. Every example is ordinary framework code.
Transport details stay in one module, while both sides agree on the data crossing the process boundary.
Component calls a domain method
Client serializes the command
FastAPI validates it
A typed result returns
import { config, http, type HttpResponse } from "graaly";
const API_URL = "http://127.0.0.1:8000";
const apiKey = String(config.get("backend.api-key"));
const options = { headers: { "x-graaly-key": apiKey } };
export type Profile = {
id: string; name: string; rank: string; coins: number;
};
export type PurchaseCommand = {
playerId: string; playerName: string; item: "diamond" | "gold";
};
export type PurchaseResult = { profile: Profile; message: string };
class ApiError extends Error {
constructor(readonly status: number, message: string) { super(message); }
}
async function read<T>(response: HttpResponse): Promise<T> {
const body = await response.json<T | { detail: string }>();
if (!response.ok) {
const message = typeof body === "object" && body !== null && "detail" in body
? String(body.detail) : `HTTP ${response.status}`;
throw new ApiError(response.status, message);
}
return body as T;
}
export const api = {
player: (id: string, name: string) =>
http.get(`${API_URL}/v1/players/${encodeURIComponent(id)}/ui?name=${encodeURIComponent(name)}`, options)
.then(read<Profile>),
purchase: (command: PurchaseCommand) =>
http.post(`${API_URL}/v1/shop/purchase`, { item: command.item }, {
...options, headers: { ...options.headers,
"idempotency-key": command.idempotencyKey },
}).then(read<PurchaseResult>),
};Do not scatter URLs, headers, untyped objects, or response parsing through UI components.
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "@graaly/react";
import { events, PlayerJoinEvent, PlayerQuitEvent } from "graaly";
import { PlayerInterface } from "./components/player-interface";
import { ShopProvider } from "./state/shop-state";
const roots = new Map();
function renderPlayer(player, shopRequest = 0) {
const id = String(player.uniqueId);
const state = roots.get(id) ?? { root: createRoot(player), query: new QueryClient() };
roots.set(id, state);
state.root.render(
<QueryClientProvider client={state.query}>
<ShopProvider>
<PlayerInterface player={player} shopRequest={shopRequest} />
</ShopProvider>
</QueryClientProvider>
);
}
events.on(PlayerJoinEvent, ({ player }) => renderPlayer(player));
events.on(PlayerQuitEvent, ({ player }) => {
const id = String(player.uniqueId);
roots.get(id)?.root.unmount();
roots.get(id)?.query.clear();
roots.delete(id);
});
// main.tsx adapts the game lifecycle only.
// Hooks, components, API client, and reducer live in focused modules.The player clicks an <Item onClick>.
React receives a typed action with player ID, slot, click type, Shift, and right-click state.
await http.post(...) runs off the tick loop; its continuation returns on the safe server thread.
FastAPI authenticates the bearer actor, resolves live permissions, validates the payload, then SQLAlchemy commits.
The result updates React state; Graaly diffs inventory slots, lines, tab, and boss bar without reopening unchanged UI.
examples/ReactFastApi.jspluginThe checked-in example includes per-player TanStack Query caches, optimistic rollback, a React role editor, JWT identity exchange, live RBAC dependencies, async SQLAlchemy models, Alembic migrations, authenticated WebSocket, generated OpenAPI TypeScript models, JavaScript and Python permission clients, health/readiness probes, and automated security, transaction, migration, socket, renderer, and contract tests.
npm install react@19.2.8 @graaly/react graaly @tanstack/react-query@5.102.1
npm install --save-dev typescript esbuild @types/react
npm run build
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -e '.[test]'
GRAALY_DATABASE_URL=sqlite+aiosqlite:///./graaly-ui.db alembic upgrade head
GRAALY_API_KEY=service-secret GRAALY_JWT_SECRET=32-byte-minimum-secret uvicorn app.main:app --host 127.0.0.1 --port 8000127.0.0.1, keep service and JWT secrets in the environment, set request timeouts, validate prices and rewards in Python, never trust an item name sent by the UI, and keep an offline fallback. Run Alembic before startup, close WebSockets in Effect cleanup, make writes idempotent, resolve permissions in FastAPI, and never grant a reward from optimistic state. Graaly cancels outstanding requests, aborts sockets, and unmounts UI when the plugin disables.Practice TypeScript, JavaScript, Python, C, React, FastAPI, Pydantic, HTML/CSS, SQL and plugin manifests. Solve Medium and Hard coding problems with executable tests, progressive hints and explained solutions, supported by 84 long-form lessons and the complete SDK reference.
208 Medium and Hard problems across 17 tracks. Write your code, run examples, then submit against edge cases. Choose a language or environment for every problem. Hints and explained solutions help when you get stuck.
SDK modules: commands, players, events, worlds, entities, tasks, http, compatibility, websocket, packets, ui, config, diagnostics, boards. Every problem supports TypeScript, JavaScript, Python, C, React, FastAPI, Pydantic, ASGI, Graaly plugin and HTML/CSS profiles. SQL and YAML provide dedicated query and configuration editors on relevant problems. Equivalent profiles use the same observable JSON contract; the selected framework profile executes its actual framework. TypeScript / JavaScript plugin tests use deterministic SDK adapters; TypeScript is transpiled without semantic type checking. React uses the repository's production renderer and test transport. Python 3.14, Pydantic 2.12 and FastAPI 0.136 run in Pyodide; SQL runs in SQLite. C17 compiles locally with Clang 22 to WebAssembly. HTML/CSS follows Graaly's native inventory subset. FastAPI exercises use async endpoints and real ASGI requests; server threads, external services and deployment are covered in the linked lessons and production examples.
Website boards remain experimental and unavailable: their exercises cover capability checks and protocol state. The API and PacketEvents reference covers the complete generated symbol catalog.
Open the complete API referenceOpen PacketEventsDownload the in-game companionDownload all problems and starter filesA teleport command accepts names containing spaces. Write the argument tokenizer before registering the command with commands.on. Support double quotes and backslash escapes without losing an empty quoted argument.
RequirementsImplement solve(input) → JSON. Preserve the caller's input. All environments run the same examples and edge cases.
{
"text": "tp \"Alex Smith\" spawn"
}Expected output[ "tp", "Alex Smith", "spawn" ]
{
"text": "say \"\" done"
}Expected output[ "say", "", "done" ]
Related theory:
Choose a course in JavaScript, TypeScript, Python, C, React, HTML/CSS, FastAPI, Pydantic, ASGI, SQL or configuration. Study the language model, inspect complete source examples and open related practice in your chosen profile. Java host lessons cover local server integration; advanced lessons connect the courses into a transactional, realtime plugin.
values, references, ownership, scanners, closures, ESM, subscription disposal, promises, stale completions, Map/Set and bounded event windows
unknown, runtime narrowing, discriminated unions, never, branded identifiers, generics, readonly borrowing, generated SDK callbacks, satisfies and semantic build checks
strict bool/int boundaries, borrowed collections, Counter, generators, Decimal rounding, aware datetime, UTC, TaskGroup, async context managers, Protocol and dataclass defaults
fixed-width integers, promotions, safe shifts, caller-owned SDK bitsets, union/intersection/difference, tail masks, tagged unions, memcpy, alignment, offsetof, padding, fieldwise equality, byte order, bounded VarInts, memmove, checked allocation, host handles and JSON algorithms
inventory metadata, stable action IDs, accessible labels, nine-column grid, spans, nested panels, cascade, specificity, source order, inherited styles, custom properties, native input, cancellation and algorithm presentation
strict fields, forbidden extras, before/after validators, cross-field invariants, aliases, nested models, discriminators, computed fields, Decimal, SecretStr, frozen models, default factories, RootModel and JsonValue
scope/receive/send, protocol routing, byte headers, bounded request chunks, disconnects, response start/body ordering, final frames and real JSON endpoint adapters
deterministic ORDER BY, LEFT JOIN, NULL, aggregates, HAVING, ranking, window frames, conditional reservations, transactions, rollback, ledger reconciliation and ordered outbox delivery
YAML scalar types, duplicate keys, runtime entrypoints, required/optional dependencies, load phases, capabilities, Java Bukkit lifecycle, thread ownership and independent host contract fixtures
components, JSX, props, children, composition, conditional rendering, lists, keys, events, useState, snapshots, immutability and batching
lifting state, single source of truth, controlled and uncontrolled components, preservation, reset, identity, useReducer, normalization and derived state
render and commit phases, reconciliation, component and render trees, pure rendering, referential equality, position and key
useEffect, dependencies, cleanup, external synchronization, race conditions, event/effect separation, useRef, ref props and imperative handles
Context, Providers, custom Hooks, memoization, Profiler, Suspense, lazy, transitions, deferred values, optimistic state, Actions, Error Boundaries and portals
feature architecture, container/presentational split, compound and headless components, CSR, SSR, hydration, streaming, Server Components and Minecraft applicability
applications, path operations, path/query/body/header/cookie/form/file input, parsing, conversion, validation, nested request models and validation errors
request/output schemas, fields, model validation, nested schemas, serialization, response validation, filtering, status, headers, cookies and custom responses
Depends, sub-dependencies, trees, caching, yield cleanup, parameterized and class dependencies, APIRouter composition, prefixes, tags and sub-applications
exception handlers, middleware ordering, lifespan, resource cleanup, background tasks, security dependencies, OAuth2 concepts, scopes and OpenAPI customization
sync/async endpoints and dependencies, threadpool behavior, WebSocket lifecycle, SSE, JSON Lines, StreamingResponse and async generators
TestClient, dependency overrides, lifespan, WebSockets, exceptions, application state, SQLAlchemy transactions and contract drift tests
TanStack Query, cache keys, invalidation, cancellation, optimistic mutations, rollback, state machines and explicit impossible states
ESM, closures, prototypes, promises and microtasks, async iterators, generators, AbortController, Error cause, WeakMap, JSDoc @ts-check, unknown, branded types, satisfies, mapped, conditional and template literal types, Result and project references
Protocol, generics, dataclasses, structural matching, async context managers, TaskGroup, cancellation, ContextVar, pytest fixtures and dependency injection
JWT identity exchange, live RBAC, service tokens, rate limiting, settings, Alembic migrations, observability, correlation IDs, reconnect, multi-server coordination, health, readiness and graceful shutdown
A reference gives access to an object; it does not grant permission to change that object.
Arrays and plain objects are shared through references. A spread creates a new outer object, but nested arrays still belong to the caller unless you copy them too. Decide which layer your function owns before sorting, normalizing or appending.
The Academy judges reject a correct answer obtained by mutating the fixture. Make a new result and copy each nested collection that you will change. Object.freeze is shallow; it is useful as a development assertion, but it cannot replace a clear ownership contract.
A loot preview must not reorder the caller's inventory or change the server's authoritative stack list.
The language and framework semantics are unchanged. Graaly owns game effects through its SDK; browser exercises verify the observable contract with isolated fixtures and the selected execution environment.
Copy only the data you will write; keep unchanged values shared when their contract is immutable.
Source example. Open the practice below to edit and run this course’s language or environment against real tests. Verify Java host examples on a local server.
Open an exercise in this course’s profile. You can choose another language or environment at any time.
The board renderer is still experimental and is not part of Graaly's stable public contract yet. Documentation and examples will return when rendering, input, scrolling, and lifecycle behavior are ready to support.
Status: planned, not available in the current release.
PacketEvents stays a separate 2.13.0 plugin. Start with practical receive, send, wrapper, cancellation, player-data, and threading guides; then search every client packet, server packet, wrapper, and supporting type below, including ClientPacket.CHAT_MESSAGE, ServerPacket.UPDATE_HEALTH, and WrapperPlayServerUpdateHealth.
The runnable PacketEventsC example includes receive/send listeners, typed wrappers, cancellation, re-encoding, ownership, and a bounded server-thread handoff. Build it with ./build.sh, then use /cpackethealth and /cpacketinfo.
8 practical guides
Use it during startup when packet features are optional or before registering packet-specific behavior.
Reports the runtime integration state without exposing PacketEvents' static singleton to script code.
Nothing
available boolean and version string or null/None
packets.availablepackets.versionPacketEvents#getAPI lifecycle and version accessimport { info, packets, warn } from "graaly";
if (!packets.available) {
warn("PacketEvents is not installed; packet features are disabled");
} else {
info(`PacketEvents ${packets.version}`);
}info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.Constants explain what travels over the protocol and generate the correct receive or send listener. Wrappers include typed C functions and constructors alongside JS, TypeScript, Python, and Java signatures.
Search the full public surface: 1421 Graaly API symbols, 289 packet wrappers,533 supporting packet types, and 288 packet constants. TypeScript, Python, and Java signatures are shown side by side, including inherited members and overloads. Select C for the SDK's typed handles, real function declarations, property writes, and C examples.
Native property rule: JavaScript and TypeScript use properties such as player.flying and player.allowFlight = true; Python uses player.flying and player.allow_flight = True. JavaBean instance accessors such as isFlying() and setFlying(...) are intentionally not exposed.
Graaly checks every compiled public type against the generated JavaScript, TypeScript, Python, and documentation catalogs. Three checked-in conformance plugins define 31 behavioral cases and compile against that same contract. Dedicated JavaScript and Python probes then run on every supplied real server from 1.7.10 through 26.2; focused Java, React, and FastAPI suites cover their own isolated boundaries.
Every compiled public top-level and nested type is present. Generated member signatures must have Java, JS, TS, and Python forms, with zero unresolved SDK types.
All 67 supplied releases load JS and Python, schedule work, dispatch commands, reload, spawn and remove an entity, adapt canonical constants, and report unavailable mechanics natively.
Join, move, chat, quit, entity effects, player-bound attributes, and packet traffic stay explicitly labeled as context-dependent; they are not presented as universal passes without the required player or PacketEvents state.
The real-server harness checks load, enable, task execution, command dispatch, repeated reload, disable, and clean shutdown in isolated directories.
Keep live world mutations on the main thread, and only install code you trust. Script plugins have the same power as JAR plugins.
Players, inventories, worlds, blocks, entities, commands, and most live server state.
Database, HTTP, file parsing, and packet inspection. Use tasks.run in JS/TS. Python coroutines run on Graaly's main loop; await tasks.to_thread(...) resumes there automatically.
Check the runtime, bundle entry point, dependency order, and thread before debugging plugin logic.
Use Oracle GraalVM 25 Innovation 2 (Graal 25.2.4 on JDK 25.0.4). A plain 25.0.4 CPU build has a different JVMCI compiler and falls back to interpreted Polyglot execution.
The entry in plugin.yml must point to compiled ESM, normally dist/main.mjs. Run the bundle build before starting the server.
No. This is a real React custom renderer whose host elements are Message, Inventory, Item, Scoreboard, Line, BossBar, and Tab. React hooks work normally; browser DOM elements belong only to a website board.
No. Graaly uses Java's asynchronous HTTP client, enforces a timeout, and never blocks the server tick. Catch the rejected promise or Python exception and render an offline fallback.
No. Keep a thin Graaly adapter in the game process. It receives events, calls FastAPI with serializable data, then safely applies the result to live players and React roots.
Install PacketEvents 2.13.0 separately and add depend: [packetevents] to the script bundle. Graaly does not redistribute it.
PacketEvents must receive cancellation and wrapper changes before its network callback returns. Graaly rejects async functions and generators at registration. A hidden Promise, coroutine, iterator, or any non-void return quarantines that listener after one diagnostic, so repeated packets cannot amplify the same stack trace. Keep the listener synchronous; use tasks.run(() => ...) in JS/TS, or capture packet data and call tasks.create_task(coroutine) in Python without returning it.
The database was temporarily unavailable or locked. Graaly's example maps SQLAlchemy operational failures to 503 Service Unavailable with Retry-After: 1. Retry a read with backoff; retry a purchase only with its original Idempotency-Key.
Use standard asyncio for sleep, gather, timeout, task creation, and cancellation. Graaly only adds tasks.create_task for synchronous entry points, sleep_ticks, to_thread, and is_main_thread.
No plugin-owned state survives: tasks, listeners, packet bindings, React roots, HTTP requests, Python modules, globals, and the event loop are cleared. JavaScript closes its Context. GraalPy keeps one isolated interpreter Context per unchanged bundle, then rebuilds its graaly facade and re-executes the current source. This avoids materializing another full Python runtime on every /reload without preserving stale plugin code or data.
Prefer pure-Python packages. Native CPython wheels may require explicit GraalPy compatibility and are not automatically portable.
Install Chrome or Chromium, verify the configured executable path, and keep the compiled site under the GraalyBoard data directory. Remote assets require an explicit HTTPS host allowlist.
Click the input or textarea, then send one chat message. GraalyBoard cancels that message before public chat and types it into the focused DOM element. Enter !cancel to abort.
Look at the board and use the mouse wheel. GraalyBoard scrolls that viewer's DOM and restores the selected hotbar slot. Tune distance with website-browser.scroll-pixels-per-step.