# HytaleJS
HytaleJS lets you write Hytale server plugins in TypeScript. Get full type safety, autocomplete, and a familiar development experience.
## Features
Write plugins with full type safety and IDE autocomplete
Listen to player joins, chat, block breaks, and 30+ events
Register custom commands with permissions
Run delayed and repeating tasks
## Quick Example
```typescript
import { EventListener, Colors } from "@hytalejs.com/core";
class MyPlugin {
@EventListener("PlayerConnectEvent")
onJoin(event: PlayerConnectEvent) {
const player = event.getPlayer();
player.sendMessage(
Message.raw("Welcome!").color(Colors.GREEN).bold(true)
);
}
}
commands.register("hello", "Say hello", (ctx) => {
ctx.sendMessage("Hello, " + ctx.getSenderName() + "!");
});
```
## Getting Started
Set up your development environment
Build a complete plugin step by step
Explore all available types and methods
# Blocks
## BlockEntity
A block with entity-like behavior in the world.
## BlockModule
Core block system module.
## BlockSetModule
Block set management.
## BlockHealth
Block health state.
## BlockHealthModule
Block health system module.
## BlockTracker
Tracks block state changes.
## BlockData
Block collision and state data.
## BlockFilter
Filter for block selection operations.
## BlockMask
Mask for block operations.
## BlockPattern
Pattern matching for blocks.
# Components
## Transform
### TransformComponent
Entity transform (position, rotation).
### PositionDataComponent
Position data component.
### HeadRotation
Head rotation component.
## Visual
### ModelComponent
Entity model component.
### DisplayNameComponent
Display name above entity.
### EntityScaleComponent
Entity scale component.
### Nameplate
Entity nameplate.
## Collision
### BoundingBox
Entity bounding box.
### CollisionResultComponent
Collision result data.
### Intangible
Makes entity non-collidable.
## Behavior
### PropComponent
Static prop component.
### Interactable
Makes entity interactable.
### DespawnComponent
Despawn timer component.
### Frozen
Freezes entity in place.
## Animation
### ActiveAnimationComponent
Currently playing animation.
### AnimationUtils
Animation utilities.
# Damage
## Damage
### DamageCause
Cause of damage (fall, attack, etc).
### DamageCalculator
Calculates final damage values.
### DamageModule
Damage system module.
### DamageDataComponent
Damage data on an entity.
## Death
### DeathComponent
Death state component.
### DeathItemLoss
Item loss on death configuration.
### KillFeedEvent
Kill feed notification event.
## Knockback
### KnockbackComponent
Knockback state component.
### KnockbackSystems
Knockback ECS systems.
## Invulnerability
### Invulnerable
Invulnerability component.
# Effects
## EntityEffect
An entity effect definition.
## ActiveEntityEffect
Currently active effect on an entity.
## EffectControllerComponent
Controls effects on an entity.
## Effect Types
### AbilityEffects
Effects triggered by abilities.
### ApplicationEffects
Effects applied on contact.
### DamageEffects
Effects triggered by damage.
### InteractionEffects
Effects from interactions.
## Effect Behavior
### OverlapBehavior
How effects stack.
### RemovalBehavior
How effects are removed.
### ModelOverride
Visual model override from effect.
# Entities
## Player
The in-world player entity.
## PlayerRef
A reference to a player that persists across worlds.
## Entity
Base entity type.
## Block Types
### BlockType
### BlockState
## Other Types
### Holder
### Ref
### GameMode
### InteractionType
# Events
## Player Connection Events
### PlayerConnectEvent
Fired when a player connects to the server.
### PlayerDisconnectEvent
Fired when a player disconnects from the server.
### PlayerSetupConnectEvent
Fired during player connection setup (before full connection).
### PlayerSetupDisconnectEvent
Fired when player disconnects during setup phase.
## Player Action Events
### PlayerChatEvent
Fired when a player sends a chat message. Can be cancelled.
### PlayerReadyEvent
Fired when a player has finished loading.
### PlayerInteractEvent
Fired when a player interacts with something.
### PlayerCraftEvent
Fired when a player crafts an item.
### PlayerMouseButtonEvent
Fired on mouse button input.
### PlayerMouseMotionEvent
Fired on mouse motion.
## World Events
### AddPlayerToWorldEvent
Fired when a player is added to a world.
### DrainPlayerFromWorldEvent
Fired when a player is removed from a world.
### PrepareUniverseEvent
Fired when the universe is being prepared.
## Block Events
### BreakBlockEvent
Fired when a block is broken. Can be cancelled.
### PlaceBlockEvent
Fired when a block is placed. Can be cancelled.
### DamageBlockEvent
Fired when a block takes damage. Can be cancelled.
### UseBlockEvent
Fired when a block is used/activated.
## Item Events
### DropItemEvent
Fired when an item is dropped. Can be cancelled.
### InteractivelyPickupItemEvent
Fired when an item is picked up. Can be cancelled.
### SwitchActiveSlotEvent
Fired when the active hotbar slot changes.
## Entity Events
### EntityEvent
Fired when an entity spawns.
### EntityRemoveEvent
Fired when an entity is removed.
### LivingEntityInventoryChangeEvent
Fired when a living entity's inventory changes.
### LivingEntityUseBlockEvent
Fired when a living entity uses a block.
## Game Mode Events
### ChangeGameModeEvent
Fired when a player's game mode changes. Can be cancelled.
## Crafting Events
### CraftRecipeEvent
Fired when a recipe is crafted. Can be cancelled.
## Permission Events
### PlayerPermissionChangeEvent
Fired when a player's permissions change.
### GroupPermissionChangeEvent
Fired when a group's permissions change.
### PlayerGroupEvent
Fired when a player's group changes.
## Server Events
### BootEvent
Fired when the server starts.
### ShutdownEvent
Fired when the server shuts down.
## Other Events
### DiscoverZoneEvent
Fired when a zone is discovered.
### PlayerRefEvent
Base event with PlayerRef.
### PlayerEvent
Base event with Player.
# API Reference
This section contains auto-generated documentation for all HytaleJS types.
30+ event types for player actions, blocks, entities, and server lifecycle
Player, PlayerRef, Entity, BlockType, BlockState
World, Chunks, WorldTime, WorldUtil
Universe, UniverseStatic
Item, ItemStack, Inventory, ItemContainer
Vector3i/f/d, Vector2i/d/f, Transform, Box, Cylinder, Color
Logger, Scheduler, Commands, Message
BlockEntity, BlockModule, BlockHealth, BlockTracker
SoundEvent, SoundCategory, PlaySoundEvent2D/3D
ParticleSystemAsset, Particle, ParticleSpawner
DynamicLight, PersistentDynamicLight
HytaleServer, TimeModule, AccessControl
Projectile, Collision, Movement, Velocity
EntityEffect, ActiveEntityEffect, AbilityEffects
DamageCause, DamageCalculator, Knockback
Transform, Model, BoundingBox, Animation
InteractionConfiguration, Selectors, Combat
# Interactions
## Core
### InteractionConfiguration
Base interaction configuration.
### InteractionManager
Manages entity interactions.
### InteractionChain
Chain of interactions.
### InteractionEntry
Single interaction entry.
### InteractionTarget
Target of an interaction.
## Block Interactions
### PlaceBlockInteraction
Place block interaction.
### DoorInteraction
Door open/close interaction.
## Combat Interactions
### DamageEntityInteraction
Damage an entity.
### ChargingInteraction
Charge-up interaction.
### LaunchProjectileInteraction
Launch a projectile.
## Effect Interactions
### ApplyEffectInteraction
Apply an effect.
### ClearEntityEffectInteraction
Clear effects from entity.
## Selectors
### RaycastSelector
Raycast target selection.
### AOECircleSelector
Area of effect circle.
### AOECylinderSelector
Area of effect cylinder.
# Inventory
## Item
An item definition from the registry.
## ItemStack
A stack of items with quantity and durability.
### Creating ItemStacks
```typescript
const stack = new ItemStack("item_id", 64);
const singleItem = new ItemStack("item_id");
```
## Inventory
A player's inventory containing multiple containers.
## ItemContainer
A container for item stacks (hotbar, storage, etc).
## CraftingRecipe
## Static Classes
### ItemClass
Access item registry via `Item.getAssetStore()`.
## Asset Types
### AssetStore
### DefaultAssetMap
### IndexedAssetMap
Used for sound events and other indexed assets.
## Java Interop
### JavaMap
### JavaSet
### JavaIterator
# Lighting
## DynamicLight
Dynamic light attached to an entity.
## PersistentDynamicLight
Persistent dynamic light component.
## DynamicLightSystems
Dynamic lighting ECS systems.
# Math
## 3D Vectors
### Vector3i
Integer 3D vector.
### Vector3f
Float 3D vector.
### Vector3d
Double 3D vector.
## 2D Vectors
### Vector2i
Integer 2D vector.
### Vector2f
Float 2D vector.
### Vector2d
Double 2D vector.
## Position
3D position in world space (double precision). Used in network packets for precise world coordinates.
## Direction
Rotation angles (yaw, pitch, roll) in degrees. Used in network packets for entity orientation.
## Transform
Position and rotation in 3D space.
## Shapes
### Box
Axis-aligned bounding box.
### Cylinder
Cylindrical shape.
## Colors
### Color
RGB color.
### ColorLight
RGBA color with alpha.
# Particles
## ParticleSystemAsset
A particle system definition.
## Particle
Individual particle configuration.
## SpawnParticleSystem
Packet for spawning particle systems.
## ParticleSpawner
Particle spawner configuration.
## ParticleSpawnerGroup
Group of particle spawners.
## ParticleAnimationFrame
Animation frame for particles.
## ParticleAttractor
Particle attractor configuration.
## ParticleCollision
Particle collision settings.
## WorldParticle
World-space particle.
# Physics
## Projectiles
### Projectile
Projectile component.
### ProjectileComponent
Legacy projectile component.
### ProjectileConfig
Projectile configuration.
### PredictedProjectile
Client-predicted projectile.
## Collision
### CollisionConfig
Collision configuration.
### HitboxCollision
Hitbox collision component.
### HitboxCollisionConfig
Hitbox configuration.
### BasicCollisionData
Basic collision data.
### BlockCollisionData
Block collision data.
### BoxCollisionData
Box collision data.
### EntityCollisionProvider
Entity collision provider.
## Movement
### MovementConfig
Movement configuration.
### MovementManager
Player movement manager.
### MovementStatesComponent
Movement state tracking.
### VelocityConfig
Velocity configuration.
## Utilities
### PhysicsMath
Physics math utilities.
# Server
## HytaleServerStatic
Static server access.
## HytaleServerInstance
Server instance methods.
## TimeModule
Server time management.
## AccessControlModule
Server access control.
## ServerPlayerListModule
Player list management.
# Sounds
## SoundEvent
A sound event definition from the registry.
## SoundEventClass
Access sound registry via `SoundEvent.getAssetMap()`.
## SoundCategory
Sound category for volume control.
## SoundCategoryEnum
## PlaySoundEvent2D
2D sound playback (UI sounds).
## PlaySoundEvent3D
3D positional sound playback.
## PlaySoundEventEntity
Sound attached to an entity.
## AudioComponent
Entity audio component.
## MovementAudioComponent
Movement-triggered audio.
# Universe
## Universe
The container for all worlds. Access via `Universe.get()`.
## UniverseStatic
Static universe access.
# Utilities
## Logger
Log messages to the server console via the global `logger`.
### Usage
```typescript
logger.info("Information message");
logger.warning("Warning message");
logger.severe("Error message");
logger.fine("Debug message");
```
## Scheduler
Run delayed and repeating tasks via the global `scheduler`.
### ScriptTask
Returned by scheduler methods to control the task.
### Usage
```typescript
scheduler.runLater(() => {
logger.info("Runs once after 5 seconds");
}, 5000);
const task = scheduler.runRepeating(() => {
logger.info("Runs every 10 seconds");
}, 10000, 10000);
task.cancel();
```
## Commands
Register custom commands via the global `commands`.
Use `commands.registerWorld(...)` for commands that should run on the world thread (e.g., heavy block edits). This avoids per-block scheduling overhead but can stall world ticks if the command runs too long.
### CommandContext
Provided to command callbacks.
### Usage
```typescript
commands.register("hello", "Say hello", (ctx) => {
ctx.sendMessage("Hello, " + ctx.getSenderName() + "!");
});
commands.register("admin", "Admin only", "admin.use", (ctx) => {
ctx.sendMessage("You have admin access!");
});
commands.registerWorld("sphere", "Run on world thread", (ctx) => {
ctx.sendMessage("This command runs on the world executor.");
});
```
## Messages
Create styled messages via the global `Message`.
### MessageStatic
### Usage
```typescript
const msg = Message.raw("Hello!")
.color(Colors.GREEN)
.bold(true);
player.sendMessage(msg);
```
## Event Handler
### EventHandler
Internal type used by the event system.
### EventType
Union of all valid event type names.
```typescript
type EventType =
| "BootEvent"
| "ShutdownEvent"
| "PlayerConnectEvent"
| "PlayerDisconnectEvent"
| "PlayerChatEvent"
// ... and 25+ more
```
# World
## World
A game world containing players and blocks.
## WorldConfigProvider
World configuration provider.
## Chunks
### ChunkTracker
Tracks chunk loading state.
### ChunkLightingManager
Manages lighting per chunk.
## Time
### WorldTimeResource
World time state.
### WorldTimeSystems
World time ECS systems.
## Utilities
### WorldUtil
World utility functions.
### WorldGenId
World generation identifier.
# Your First Plugin
import { Step, Steps } from 'fumadocs-ui/components/steps';
Let's build a plugin that welcomes players, formats chat, and adds a `/players` command.
### Create src/main.ts
```typescript
import { type PlayerConnectEvent, type PlayerChatEvent, EventListener, Colors } from "@hytalejs.com/core";
```
### Add a welcome message on join
```typescript
class MyPlugin {
@EventListener("PlayerConnectEvent")
onJoin(event: PlayerConnectEvent) {
const player = event.getPlayer();
const name = event.getPlayerRef().getUsername();
player.sendMessage(
Message.raw("Welcome, " + name + "!").color(Colors.GREEN).bold(true)
);
Universe.get().sendMessage(
Message.raw(name + " joined the server!")
);
}
}
```
### Format chat messages
```typescript
@EventListener("PlayerChatEvent")
onChat(event: PlayerChatEvent) {
const sender = event.getSender();
const content = event.getContent();
const formatted = Message.empty()
.insert(Message.raw(sender.getUsername()).color(Colors.GOLD).bold(true))
.insert(Message.raw(": ").color(Colors.GRAY))
.insert(Message.raw(content).color(Colors.WHITE));
event.setCancelled(true);
const targets = event.getTargets();
for (let i = 0; i < targets.length; i++) {
targets[i].sendMessage(formatted);
}
}
```
### Register a command
```typescript
commands.register("players", "List online players", (ctx) => {
const universe = Universe.get();
ctx.sendMessage("Online: " + universe.getPlayerCount());
const players = universe.getPlayers();
for (let i = 0; i < players.length; i++) {
ctx.sendMessage("- " + players[i].getUsername());
}
});
```
### Build and deploy
```bash
npm run build
cp dist/plugin.js /path/to/hytale-server/mods/bmstefanski_HytaleJS/scripts/
```
Restart your server to load the plugin.
## Complete Code
```typescript
import { type PlayerConnectEvent, type PlayerChatEvent, EventListener, Colors } from "@hytalejs.com/core";
class MyPlugin {
@EventListener("PlayerConnectEvent")
onJoin(event: PlayerConnectEvent) {
const player = event.getPlayer();
const name = event.getPlayerRef().getUsername();
player.sendMessage(
Message.raw("Welcome, " + name + "!").color(Colors.GREEN).bold(true)
);
Universe.get().sendMessage(Message.raw(name + " joined the server!"));
}
@EventListener("PlayerChatEvent")
onChat(event: PlayerChatEvent) {
const sender = event.getSender();
const content = event.getContent();
const formatted = Message.empty()
.insert(Message.raw(sender.getUsername()).color(Colors.GOLD).bold(true))
.insert(Message.raw(": ").color(Colors.GRAY))
.insert(Message.raw(content).color(Colors.WHITE));
event.setCancelled(true);
const targets = event.getTargets();
for (let i = 0; i < targets.length; i++) {
targets[i].sendMessage(formatted);
}
}
}
commands.register("players", "List online players", (ctx) => {
const universe = Universe.get();
ctx.sendMessage("Online: " + universe.getPlayerCount());
const players = universe.getPlayers();
for (let i = 0; i < players.length; i++) {
ctx.sendMessage("- " + players[i].getUsername());
}
});
```
## Next Steps
Learn about all available events
Add permissions and parse arguments
# Installation
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';
## Server Setup
### Download HytaleJS
Download the latest `HytaleJS.jar` from the [releases page](https://github.com/bmstefanski/HytaleJS/releases).
### Install the plugin
Copy `HytaleJS.jar` to your Hytale server's mods folder:
```
hytale-server/mods/HytaleJS.jar
```
### Start your server
The plugin will create a scripts folder at:
```
hytale-server/mods/bmstefanski_HytaleJS/scripts/
```
All `.js` files in the scripts folder are automatically loaded when the server starts.
## Development Setup
To write plugins in TypeScript with type checking and autocompletion:
### Prerequisites
* Node.js 18+
### Setup
### Create a new project
```bash
mkdir my-plugin && cd my-plugin
npm init -y
```
### Install dependencies
```bash
pnpm add @hytalejs.com/core
pnpm add -D typescript esbuild
```
```bash
npm install @hytalejs.com/core
npm install -D typescript esbuild
```
```bash
bun add @hytalejs.com/core
bun add -D typescript esbuild
```
### Create tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"experimentalDecorators": true,
"types": ["@hytalejs.com/core"]
},
"include": ["src"]
}
```
### Add build script to package.json
```json
{
"scripts": {
"build": "esbuild src/main.ts --bundle --format=esm --target=es2022 --outfile=dist/plugin.js"
}
}
```
## Project Structure
```
my-plugin/
├── src/
│ └── main.ts # Your plugin code
├── dist/
│ └── plugin.js # Built output (deploy this)
├── package.json
└── tsconfig.json
```
## Deploying
Copy your built `dist/plugin.js` to the HytaleJS scripts folder:
```
hytale-server/mods/bmstefanski_HytaleJS/scripts/plugin.js
```
Restart your server to load the plugin.
## Next Steps
Build a complete plugin with events and commands
# Block Manipulation
Modify the game world by placing, breaking, and reading blocks.
## Reading Blocks
Get the block type at any position:
```typescript
const world = Universe.get().getDefaultWorld();
const blockType = world.getBlockType(100, 64, 200);
if (blockType) {
ctx.sendMessage("Block: " + blockType.getId());
}
```
You can also use a `Vector3i`:
```typescript
const pos = new Vector3i(100, 64, 200);
const blockType = world.getBlockType(pos);
```
## Placing Blocks
Place a single block at coordinates:
```typescript
const world = Universe.get().getDefaultWorld();
world.setBlock(100, 64, 200, "Cloth_Block_Wool_black");
```
With rotation (0-3):
```typescript
world.setBlock(100, 64, 200, "Cloth_Block_Wool_black", 2);
```
## Breaking Blocks
Break a block with a harvest level:
```typescript
const world = Universe.get().getDefaultWorld();
const success = world.breakBlock(100, 64, 200, 1);
if (success) {
ctx.sendMessage("Block broken!");
}
```
## Building Shapes
Create a platform at the player's position:
```typescript
const pos = playerRef.getTransform().getPosition();
const baseX = Math.floor(pos.getX());
const baseY = Math.floor(pos.getY()) - 1;
const baseZ = Math.floor(pos.getZ());
const world = Universe.get().getDefaultWorld();
const size = 5;
for (let x = 0; x < size; x++) {
for (let z = 0; z < size; z++) {
world.setBlock(baseX + x, baseY, baseZ + z, "Cloth_Block_Wool_black");
}
}
```
## Batched Block Updates
For large-scale building, individual `setBlock` calls are slow because each one sends a network packet. Use `ServerSetBlocks` to batch multiple block changes into a single packet.
### How It Works
Hytale divides the world into 32x32x32 block sections. The `ServerSetBlocks` packet sends multiple block changes within a section at once. Use `ChunkUtil` for section calculations:
1. Get section coordinates: `ChunkUtil.chunkCoordinate(blockPos)`
2. Get block index within section: `ChunkUtil.indexBlock(x, y, z)`
3. Check if blocks are in the same section: `ChunkUtil.isSameChunkSection(...)`
4. Send packet to all players
### Getting Block IDs
Convert a block name to its numeric ID:
```typescript
const blockId = BlockType.getBlockIdOrUnknown(
"Cloth_Block_Wool_black",
"Unknown block: %s",
"Cloth_Block_Wool_black"
);
```
### Creating Block Commands
Each block in the batch needs a `SetBlockCmd`:
```typescript
const index = ChunkUtil.indexBlock(worldX, worldY, worldZ);
const cmd = new SetBlockCmd(index, blockId, 0, 0);
```
Parameters: `(index, blockId, filler, rotation)`
### Sending the Packet
```typescript
const sectionX = ChunkUtil.chunkCoordinate(baseX);
const sectionY = ChunkUtil.chunkCoordinate(baseY);
const sectionZ = ChunkUtil.chunkCoordinate(baseZ);
const packet = new ServerSetBlocks(sectionX, sectionY, sectionZ, cmds);
const players = Universe.get().getPlayers();
for (let i = 0; i < players.length; i++) {
players[i].getPacketHandler().write(packet);
}
```
### Complete Example
Build a platform using batched updates:
```typescript
const pos = playerRef.getTransform().getPosition();
const baseX = Math.floor(pos.getX());
const baseY = Math.floor(pos.getY()) - 1;
const baseZ = Math.floor(pos.getZ());
const blockId = BlockType.getBlockIdOrUnknown("Cloth_Block_Wool_black", "Unknown block");
const sectionX = ChunkUtil.chunkCoordinate(baseX);
const sectionY = ChunkUtil.chunkCoordinate(baseY);
const sectionZ = ChunkUtil.chunkCoordinate(baseZ);
const cmds = [];
const size = 10;
for (let dx = 0; dx < size; dx++) {
for (let dz = 0; dz < size; dz++) {
const worldX = baseX + dx;
const worldZ = baseZ + dz;
if (!ChunkUtil.isSameChunkSection(baseX, baseY, baseZ, worldX, baseY, worldZ)) {
continue;
}
const index = ChunkUtil.indexBlock(worldX, baseY, worldZ);
cmds.push(new SetBlockCmd(index, blockId, 0, 0));
}
}
const packet = new ServerSetBlocks(sectionX, sectionY, sectionZ, cmds);
const players = Universe.get().getPlayers();
for (let i = 0; i < players.length; i++) {
players[i].getPacketHandler().write(packet);
}
```
### Limitations
* Maximum 32,768 blocks per packet (one full 32x32x32 section)
* Blocks must be in the same section
* For builds spanning multiple sections, send one packet per section
## When to Use Each Approach
| Approach | Use Case |
| ------------------ | --------------------------------------------- |
| `world.setBlock()` | Small changes, single blocks |
| `ServerSetBlocks` | Large builds, performance-critical operations |
For most commands placing a few blocks, `world.setBlock()` is simpler and works fine. Use batched updates when building large structures where performance matters.
# Commands
Create custom commands that players can run in-game.
## Basic Command
```typescript
commands.register("hello", "Say hello", (ctx) => {
ctx.sendMessage("Hello, " + ctx.getSenderName() + "!");
});
```
## With Permission
```typescript
commands.register("heal", "Heal yourself", "admin.heal", (ctx) => {
ctx.sendMessage("You have been healed!");
});
```
Players need the `admin.heal` permission to use this command.
## World-Thread Commands
Some operations (like large block edits) are significantly faster when they run on the **world thread**. Use `commands.registerWorld(...)` to execute a command on the world executor.
```typescript
commands.registerWorld("sphere", "Build a sphere on the world thread", (ctx) => {
// Heavy block edits here
});
```
### Why This Exists
Hytale's world state is updated on its own thread. When commands run off-thread, each `setBlock` can incur extra scheduling and synchronization overhead. `registerWorld` runs the JS callback on the world executor, which avoids per-block re-queueing and matches the fastest native command path.
### Caveats
* **Blocks the world thread:** long loops will pause world ticks while the command runs.
* **Use sparingly:** prefer batching (`ServerSetBlocks`) for very large edits and keep world-thread commands short where possible.
## Parsing Arguments
Access the full command input and parse arguments:
```typescript
commands.register("give", "Give items", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
if (parts.length < 2) {
ctx.sendMessage("Usage: /give [quantity]");
return;
}
const itemId = parts[1];
const quantity = parts.length >= 3 ? parseInt(parts[2], 10) : 1;
if (isNaN(quantity) || quantity < 1) {
ctx.sendMessage("Invalid quantity");
return;
}
ctx.sendMessage("Giving " + quantity + "x " + itemId);
});
```
## Finding the Player
Get the player entity from the command sender:
```typescript
commands.register("pos", "Show your position", (ctx) => {
const senderName = ctx.getSenderName();
const world = Universe.get().getDefaultWorld();
const players = world.getPlayers();
let player = null;
for (let i = 0; i < players.length; i++) {
if (players[i].getDisplayName() === senderName) {
player = players[i];
break;
}
}
if (!player) {
ctx.sendMessage("Could not find player");
return;
}
const ref = player.getPlayerRef();
const transform = ref.getTransform();
const pos = transform.getPosition();
ctx.sendMessage("Position: " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ());
});
```
## Full Example
```typescript
commands.register("serverinfo", "Show server info", (ctx) => {
ctx.sendMessage("Server: " + HytaleServer.get().getServerName());
ctx.sendMessage("Players: " + Universe.get().getPlayerCount());
ctx.sendMessage("World: " + Universe.get().getDefaultWorld().getName());
});
commands.register("items", "List items", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
const filter = parts.length >= 2 ? parts[1].toLowerCase() : "";
const map = Item.getAssetStore().getAssetMap().getAssetMap();
const keys = map.keySet();
const iterator = keys.iterator();
let count = 0;
while (iterator.hasNext() && count < 20) {
const key = iterator.next() as string;
if (!filter || key.toLowerCase().includes(filter)) {
ctx.sendMessage(key);
count++;
}
}
});
```
# Events
HytaleJS provides 30+ events for player actions, world changes, and server lifecycle.
## Basic Usage
Use the `@EventListener` decorator to handle events:
```typescript
import { type PlayerConnectEvent, EventListener } from "@hytalejs.com/core";
class MyPlugin {
@EventListener("PlayerConnectEvent")
onJoin(event: PlayerConnectEvent) {
const player = event.getPlayer();
player.sendMessage(Message.raw("Welcome!"));
}
}
```
## Cancelling Events
Some events can be cancelled to prevent the default action:
```typescript
@EventListener("PlayerChatEvent")
onChat(event: PlayerChatEvent) {
if (event.getContent().includes("badword")) {
event.setCancelled(true);
event.getSender().sendMessage(Message.raw("Watch your language!"));
}
}
```
## Common Events
### Player Events
| Event | Description |
| ----------------------- | ------------------------------- |
| `PlayerConnectEvent` | Player joins the server |
| `PlayerDisconnectEvent` | Player leaves the server |
| `PlayerChatEvent` | Player sends a chat message |
| `PlayerReadyEvent` | Player finished loading |
| `PlayerInteractEvent` | Player interacts with something |
### Block Events
| Event | Description |
| ------------------ | ----------------------- |
| `BreakBlockEvent` | Block is broken |
| `PlaceBlockEvent` | Block is placed |
| `DamageBlockEvent` | Block takes damage |
| `UseBlockEvent` | Block is used/activated |
### Entity Events
| Event | Description |
| ------------------- | ----------------- |
| `EntityEvent` | Entity spawns |
| `EntityRemoveEvent` | Entity is removed |
### Server Events
| Event | Description |
| ---------------------- | -------------------------- |
| `BootEvent` | Server starts |
| `ShutdownEvent` | Server stops |
| `PrepareUniverseEvent` | Universe is being prepared |
## Full Example
```typescript
import {
type PlayerConnectEvent,
type PlayerDisconnectEvent,
type BreakBlockEvent,
EventListener,
Colors
} from "@hytalejs.com/core";
class EventsPlugin {
@EventListener("PlayerConnectEvent")
onJoin(event: PlayerConnectEvent) {
logger.info("Player joined: " + event.getPlayerRef().getUsername());
}
@EventListener("PlayerDisconnectEvent")
onLeave(event: PlayerDisconnectEvent) {
logger.info("Player left: " + event.getPlayerRef().getUsername());
}
@EventListener("BreakBlockEvent")
onBreak(event: BreakBlockEvent) {
const block = event.getTargetBlock();
logger.info("Block broken at: " + block.getX() + ", " + block.getY() + ", " + block.getZ());
}
}
```
See the [Events API Reference](/docs/api/events) for all event types and their properties.
# Messages
Create rich, formatted messages with colors and styling.
## Basic Message
```typescript
const msg = Message.raw("Hello, world!");
player.sendMessage(msg);
```
## Colors
Use the `Colors` constant or hex codes:
```typescript
import { Colors } from "@hytalejs.com/core";
Message.raw("Success!").color(Colors.GREEN)
Message.raw("Warning!").color(Colors.YELLOW)
Message.raw("Error!").color(Colors.RED)
Message.raw("Custom").color("#FF5733")
```
### Available Colors
| Color | Hex |
| ---------------- | ------- |
| `Colors.RED` | #FF0000 |
| `Colors.GREEN` | #00FF00 |
| `Colors.BLUE` | #0000FF |
| `Colors.YELLOW` | #FFFF00 |
| `Colors.CYAN` | #00FFFF |
| `Colors.MAGENTA` | #FF00FF |
| `Colors.WHITE` | #FFFFFF |
| `Colors.BLACK` | #000000 |
| `Colors.ORANGE` | #FFA500 |
| `Colors.PINK` | #FFC0CB |
| `Colors.PURPLE` | #800080 |
| `Colors.GOLD` | #FFD700 |
| `Colors.GRAY` | #808080 |
## Styling
```typescript
Message.raw("Bold text").bold(true)
Message.raw("Italic text").italic(true)
Message.raw("Bold and italic").bold(true).italic(true)
```
## Chaining
Build complex messages by chaining methods:
```typescript
const welcome = Message.raw("Welcome!")
.color(Colors.GREEN)
.bold(true);
```
## Combining Messages
Use `Message.empty()` and `insert()` to combine multiple styled parts:
```typescript
const formatted = Message.empty()
.insert(Message.raw("Player").color(Colors.GOLD).bold(true))
.insert(Message.raw(": ").color(Colors.GRAY))
.insert(Message.raw("Hello everyone!").color(Colors.WHITE));
```
## Sending Messages
### To a specific player
```typescript
player.sendMessage(Message.raw("Private message"));
```
### To all players in a world
```typescript
const world = Universe.get().getDefaultWorld();
world.sendMessage(Message.raw("World announcement"));
```
### To all players on the server
```typescript
Universe.get().sendMessage(Message.raw("Server announcement"));
```
## Full Example
```typescript
@EventListener("PlayerChatEvent")
onChat(event: PlayerChatEvent) {
const sender = event.getSender();
const content = event.getContent();
const playerName = Message.raw(sender.getUsername())
.color(Colors.GOLD)
.bold(true);
const separator = Message.raw(": ").color(Colors.GRAY);
const text = Message.raw(content).color(Colors.WHITE);
const formatted = Message.empty()
.insert(playerName)
.insert(separator)
.insert(text);
event.setCancelled(true);
const targets = event.getTargets();
for (let i = 0; i < targets.length; i++) {
targets[i].sendMessage(formatted);
}
}
```
# Particles
Spawn particle systems in the world with customizable properties like position, rotation, scale, and color.
## Spawning a Particle System
```typescript
const position = new Position(100, 64, -50);
const rotation = new Direction(0, 0, 0);
const color = new Color(255, 0, 0); // Red (requires byte conversion)
const packet = new SpawnParticleSystem(
"Fire_ChargeRed",
position,
rotation,
1.0, // Scale
color
);
playerRef.getPacketHandler().write(packet);
```
## Color Conversion
The `Color` class uses Java signed bytes (-128 to 127) internally, but you provide RGB values (0-255). Values above 127 must be converted:
```typescript
// Helper to convert 0-255 RGB to signed bytes
let r = 255, g = 100, b = 50;
// Convert values > 127 to negative bytes
if (r > 127) r = r - 256; // 255 → -1
if (g > 127) g = g - 256; // 100 stays 100
if (b > 127) b = b - 256; // 50 stays 50
const color = new Color(r, g, b);
```
**Common conversions:**
* `255` (bright) → `-1`
* `200` → `-56`
* `128` → `-128`
* `127` and below → no conversion needed
## Listing Available Particles
```typescript
commands.register("particlelist", "List particles", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
const filter = parts.length >= 2 ? parts[1].toLowerCase() : "";
const assetMap = ParticleSystem.getAssetMap();
const map = assetMap.getAssetMap();
const keys = map.keySet();
const iterator = keys.iterator();
let count = 0;
const maxResults = 20;
while (iterator.hasNext() && count < maxResults) {
const key = iterator.next() as string;
if (!filter || key.toLowerCase().includes(filter)) {
ctx.sendMessage(key);
count++;
}
}
const total = assetMap.getAssetCount();
ctx.sendMessage(`Showing ${count} of ${total} particles`);
});
```
## Full Example: Particle Command
```typescript
commands.register("particle", "Spawn a particle system", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
if (parts.length < 2) {
ctx.sendMessage("Usage: /particle [scale] [r] [g] [b]");
return;
}
const particleId = parts[1];
const scale = parts.length >= 3 ? parseFloat(parts[2]) : 1.0;
let r = parts.length >= 4 ? parseInt(parts[3], 10) : 255;
let g = parts.length >= 5 ? parseInt(parts[4], 10) : 255;
let b = parts.length >= 6 ? parseInt(parts[5], 10) : 255;
// Validate inputs
if (isNaN(scale) || scale <= 0) {
ctx.sendMessage("Invalid scale");
return;
}
if (isNaN(r) || r < 0 || r > 255 || isNaN(g) || g < 0 || g > 255 || isNaN(b) || b < 0 || b > 255) {
ctx.sendMessage("Invalid RGB values (0-255)");
return;
}
// Convert RGB to signed bytes
if (r > 127) r = r - 256;
if (g > 127) g = g - 256;
if (b > 127) b = b - 256;
// Find the player
const senderName = ctx.getSenderName();
const players = Universe.get().getPlayers();
let playerRef = null;
for (let i = 0; i < players.length; i++) {
if (players[i].getUsername() === senderName) {
playerRef = players[i];
break;
}
}
if (!playerRef) {
ctx.sendMessage("Could not find player");
return;
}
// Get player position and spawn particles above them
const transform = playerRef.getTransform();
const pos = transform.getPosition();
const rot = transform.getRotation();
const position = new Position(pos.getX(), pos.getY() + 2.0, pos.getZ());
const direction = new Direction(rot.getX(), rot.getY(), rot.getZ());
const color = new Color(r, g, b);
const packet = new SpawnParticleSystem(particleId, position, direction, scale, color);
playerRef.getPacketHandler().write(packet);
ctx.sendMessage(`Spawned particle: ${particleId}`);
});
```
## Example Usage
```bash
# Spawn default white fire charge
/particle Fire_ChargeRed
# Large red particles (scale 2.0, RGB 255,0,0)
/particle Fire_ChargeRed 2.0 255 0 0
# Blue fire charge (scale 1.5, RGB 0,100,255)
/particle Fire_ChargeRed 1.5 0 100 255
# Purple fire charge (RGB 200,0,200)
/particle Fire_ChargeRed 1.0 200 0 200
```
## Particle Properties
### Position
* **X**: East (+) / West (-)
* **Y**: Up (+) / Down (-)
* **Z**: South (+) / North (-)
### Scale
* Multiplier for particle size
* Typical range: `0.5` to `5.0`
* `1.0` = default size
### Color Tinting
* RGB values: `0` to `255`
* Automatically converted to signed bytes for Java
* Tints the particle texture
## Broadcasting to All Players
To show particles to everyone:
```typescript
const players = Universe.get().getPlayers();
for (let i = 0; i < players.length; i++) {
players[i].getPacketHandler().write(packet);
}
```
# Scheduler
Execute code after a delay or on a repeating schedule.
## Run After Delay
Execute code once after a delay:
```typescript
scheduler.runLater(() => {
logger.info("This runs after 5 seconds");
}, 5000);
```
## Repeating Task
Run code repeatedly at a fixed interval:
```typescript
scheduler.runRepeating(() => {
logger.info("This runs every 10 seconds");
}, 10000, 10000);
```
Parameters: `callback`, `initialDelay`, `period` (all in milliseconds)
## Repeating with Initial Delay
```typescript
scheduler.runRepeatingWithDelay(() => {
logger.info("Starts after 5s, then every 10s");
}, 5000, 10000);
```
## Cancelling Tasks
Store the task reference to cancel it later:
```typescript
const task = scheduler.runRepeating(() => {
logger.info("Running...");
}, 1000, 1000);
scheduler.runLater(() => {
task.cancel();
logger.info("Task cancelled");
}, 10000);
```
## Task Methods
| Method | Description |
| --------------- | ------------------ |
| `cancel()` | Stop the task |
| `isCancelled()` | Check if cancelled |
| `isDone()` | Check if completed |
## Full Example: Auto Broadcast
```typescript
const messages = [
"Remember to stay hydrated!",
"Join our Discord for updates!",
"Try the /items command!",
"Found a bug? Report it!",
];
scheduler.runRepeating(() => {
if (Universe.get().getPlayerCount() > 0) {
const index = Math.floor(Math.random() * messages.length);
const msg = Message.raw("[Auto] " + messages[index])
.color(Colors.CYAN)
.italic(true);
Universe.get().sendMessage(msg);
}
}, 15000, 15000);
```
## Full Example: Countdown
```typescript
commands.register("countdown", "Start a countdown", (ctx) => {
let count = 5;
const task = scheduler.runRepeating(() => {
if (count > 0) {
Universe.get().sendMessage(
Message.raw(count + "...").color(Colors.YELLOW)
);
count--;
} else {
Universe.get().sendMessage(
Message.raw("Go!").color(Colors.GREEN).bold(true)
);
task.cancel();
}
}, 0, 1000);
});
```
# Sounds
Play sound effects to players using the sound system. HytaleJS supports both 2D (non-positional) and 3D (positional) sound playback.
## Playing a 2D Sound
2D sounds are played without spatial audio - all players hear them at the same volume regardless of position.
```typescript
const assetMap = SoundEvent.getAssetMap();
const soundIndex = assetMap.getIndex("SFX_Global_Weather_Thunder");
const packet = new PlaySoundEvent2D(
soundIndex,
SoundCategory.SFX,
1.0, // Volume
1.0 // Pitch
);
playerRef.getPacketHandler().write(packet);
```
## Playing a 3D Sound
3D sounds are played at a specific world position with spatial audio. Players hear the sound with volume and panning based on their distance and direction from the source.
```typescript
const assetMap = SoundEvent.getAssetMap();
const soundIndex = assetMap.getIndex("SFX_Global_Weather_Thunder");
// Create a position in world space
const position = new Position(100, 64, -50);
const packet = new PlaySoundEvent3D(
soundIndex,
SoundCategory.SFX,
position,
1.0, // Volume modifier
1.0 // Pitch modifier
);
playerRef.getPacketHandler().write(packet);
```
## Playing Sound Attached to Entity
Play a sound that follows an entity as it moves:
```typescript
const soundIndex = SoundEvent.getAssetMap().getIndex("SFX_Global_Weather_Thunder");
const packet = new PlaySoundEventEntity(
soundIndex,
entityNetworkId, // Entity's network ID
1.0, // Volume
1.0 // Pitch
);
playerRef.getPacketHandler().write(packet);
```
## Sound Categories
| Category | Use |
| ----------------------- | -------------------- |
| `SoundCategory.Music` | Background music |
| `SoundCategory.Ambient` | Environmental sounds |
| `SoundCategory.SFX` | Sound effects |
| `SoundCategory.UI` | Interface sounds |
## Volume and Pitch
* **Volume**: `0.0` to `2.0` (1.0 = normal)
* **Pitch**: `0.5` to `2.0` (1.0 = normal)
```typescript
new PlaySoundEvent2D(soundIndex, SoundCategory.SFX, 0.5, 1.5)
```
## Listing Available Sounds
```typescript
commands.register("soundlist", "List sounds", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
const filter = parts.length >= 2 ? parts[1].toLowerCase() : "";
const assetMap = SoundEvent.getAssetMap();
const map = assetMap.getAssetMap();
const keys = map.keySet();
const iterator = keys.iterator();
let count = 0;
while (iterator.hasNext() && count < 20) {
const key = iterator.next() as string;
if (!filter || key.toLowerCase().includes(filter)) {
ctx.sendMessage(key);
count++;
}
}
ctx.sendMessage("Showing " + count + " of " + assetMap.getAssetCount() + " sounds");
});
```
## Full Example: Playsound Command
```typescript
commands.register("playsound", "Play a sound", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
if (parts.length < 2) {
ctx.sendMessage("Usage: /playsound [volume] [pitch]");
return;
}
const soundId = parts[1];
const volume = parts.length >= 3 ? parseFloat(parts[2]) : 1.0;
const pitch = parts.length >= 4 ? parseFloat(parts[3]) : 1.0;
if (isNaN(volume) || volume < 0 || volume > 2) {
ctx.sendMessage("Invalid volume (0-2)");
return;
}
if (isNaN(pitch) || pitch < 0.5 || pitch > 2) {
ctx.sendMessage("Invalid pitch (0.5-2)");
return;
}
const assetMap = SoundEvent.getAssetMap();
const soundIndex = assetMap.getIndex(soundId);
if (soundIndex < 0) {
ctx.sendMessage("Sound not found: " + soundId);
return;
}
const senderName = ctx.getSenderName();
const players = Universe.get().getPlayers();
let playerRef = null;
for (let i = 0; i < players.length; i++) {
if (players[i].getUsername() === senderName) {
playerRef = players[i];
break;
}
}
if (!playerRef) {
ctx.sendMessage("Could not find player");
return;
}
const packet = new PlaySoundEvent2D(soundIndex, SoundCategory.SFX, volume, pitch);
playerRef.getPacketHandler().write(packet);
ctx.sendMessage("Playing: " + soundId);
});
```
## Full Example: 3D Playsound Command
```typescript
commands.register("playsound3d", "Play a sound at your position", (ctx) => {
const input = ctx.getInput();
const parts = input.split(" ");
if (parts.length < 2) {
ctx.sendMessage("Usage: /playsound3d [volume] [pitch]");
return;
}
const soundId = parts[1];
const volume = parts.length >= 3 ? parseFloat(parts[2]) : 1.0;
const pitch = parts.length >= 4 ? parseFloat(parts[3]) : 1.0;
const assetMap = SoundEvent.getAssetMap();
const soundIndex = assetMap.getIndex(soundId);
if (soundIndex < 0) {
ctx.sendMessage("Sound not found: " + soundId);
return;
}
const senderName = ctx.getSenderName();
const players = Universe.get().getPlayers();
let playerRef = null;
for (let i = 0; i < players.length; i++) {
if (players[i].getUsername() === senderName) {
playerRef = players[i];
break;
}
}
if (!playerRef) {
ctx.sendMessage("Could not find player");
return;
}
// Get player's position for 3D sound
const transform = playerRef.getTransform();
const pos = transform.getPosition();
const position = new Position(pos.getX(), pos.getY(), pos.getZ());
const packet = new PlaySoundEvent3D(soundIndex, SoundCategory.SFX, position, volume, pitch);
playerRef.getPacketHandler().write(packet);
ctx.sendMessage("Playing 3D sound: " + soundId);
});
```