# 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); }); ```