Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add ZacxDev/claude-hytale-mod --skill "hytale-mod"
Install specific skill from multi-skill repository
# Description
Hytale server plugin and modding development. Use for Java plugins, commands, events, Custom UI, asset packs, items, weapons, and animations.
# SKILL.md
name: hytale-mod
description: Hytale server plugin and modding development. Use for Java plugins, commands, events, Custom UI, asset packs, items, weapons, and animations.
allowed-tools: [Read, Grep, Glob, Bash, Edit, Write, Task]
argument-hint: "[task description]"
Hytale Modding Skill
Prime context for Hytale server plugin development and modding (Early Access, Dec 2025).
Environment Requirements
- Java 25 (required by Hytale server)
- Gradle Groovy DSL only β Kotlin DSL (
.gradle.kts) fails with JDK 25 - Maven coordinates:
com.hypixel.hytale:Server:2026.01.28-87d03be09
Project Structure
plugin-name/
βββ build.gradle # Groovy DSL, shadowJar
βββ src/main/
β βββ java/ # Plugin code
β βββ resources/
β βββ manifest.json # Plugin manifest
β βββ Server/ # Data assets (items, blocks)
β βββ Common/ # Visual assets (icons, UI, textures)
Gradle Configuration
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
repositories {
mavenCentral()
maven {
name = "hytale-release"
url = uri("https://maven.hytale.com/release")
}
maven {
name = "hytale-pre-release"
url = uri("https://maven.hytale.com/pre-release")
}
}
dependencies {
compileOnly("com.hypixel.hytale:Server:2026.01.28-87d03be09")
}
Plugin Lifecycle
package com.example.myplugin;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
import javax.annotation.Nonnull;
public class MyPlugin extends JavaPlugin {
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
getLogger().atInfo().log("Plugin loaded!");
}
@Override
protected void setup() {
// Register commands, events, components
getLogger().atInfo().log("Plugin setup!");
}
@Override
protected void start() {
// Start tasks, load config
getLogger().atInfo().log("Plugin started!");
}
@Override
protected void shutdown() {
// Save data, clean up resources
getLogger().atInfo().log("Plugin shutdown!");
}
}
Critical Gotchas
Logging (Flogger, not SLF4J)
getLogger().atInfo().log("message"); // CORRECT
getLogger().info("message"); // WRONG - method doesn't exist
Event Registration
| Event Type | Method | Example |
|---|---|---|
IBaseEvent<Void> |
register(Class, Consumer) |
PlayerConnectEvent |
IAsyncEvent<K> |
registerGlobal(Class, Consumer) |
PlayerChatEvent |
Using register() with keyed events fails at compile time.
Command Permissions
Commands auto-generate permissions. Make public with:
@Override
protected boolean canGeneratePermission() {
return false;
}
PNG Textures
All PNGs MUST be 8-bit/channel RGBA. 16-bit crashes the client.
magick input.png -depth 8 -type TrueColorAlpha PNG32:output.png
file texture.png # Should show "8-bit/color RGBA"
Asset Pack Paths
- Item definitions:
Server/Item/Items/(NOTCommon/Assets/Items/) - Item icons:
Common/Icons/ItemsGenerated/ - Item models:
Common/Items/ItemName/ItemName.blockymodel - Item textures:
Common/Items/ItemName/ItemName_Texture.png - UI files:
Common/UI/Custom/ - Mods folder:
server/Server/mods/(NOTserver/mods/)
Custom Items (CRITICAL)
Items REQUIRE an Id field matching the filename:
{
"Id": "My_Item",
"TranslationProperties": { "Name": "My Item" },
"Icon": "Icons/ItemsGenerated/My_Item.png",
"Model": "Items/My_Item/My_Item.blockymodel",
"Texture": "Items/My_Item/My_Item_Texture.png"
}
- Even though server logs warn "Unused key(s): Id", items without
Idare silently ignored - Model/Texture paths must reference bundled assets β you cannot use vanilla asset paths
- Items are namespaced:
/spawnitem pluginname:ItemId - Commands are namespaced:
/pluginname:commandname
Command Pattern
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import java.util.concurrent.CompletableFuture;
public class MyCommand extends AbstractCommand {
private final OptionalArg<String> nameArg;
public MyCommand() {
super("mycommand", "Description of my command");
nameArg = withOptionalArg("name", "A name argument", ArgTypes.STRING);
}
@Override
protected CompletableFuture<Void> execute(CommandContext ctx) {
String name = ctx.get(nameArg);
ctx.sendMessage(Message.raw("Hello, " + (name != null ? name : "world") + "!"));
return CompletableFuture.completedFuture(null);
}
}
// Register in setup():
// getCommandRegistry().registerCommand(new MyCommand());
Event Pattern
// Unkeyed event (PlayerConnectEvent implements IBaseEvent<Void>)
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef player = event.getPlayerRef();
player.sendMessage(Message.raw("Welcome, " + player.getUsername() + "!"));
});
// Keyed event (PlayerChatEvent implements IAsyncEvent<String>)
// MUST use registerGlobal - register() won't compile
getEventRegistry().registerGlobal(PlayerChatEvent.class, event -> {
String content = event.getContent();
PlayerRef sender = event.getSender();
});
Custom UI Pattern
InteractiveCustomUIPage (with input)
import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.*;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.protocol.packets.interface_.*;
public class MyPage extends InteractiveCustomUIPage<MyPage.EventData> {
public MyPage(PlayerRef playerRef) {
super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC);
}
@Override
public void build(Ref<EntityStore> ref, UICommandBuilder ui,
UIEventBuilder events, Store<EntityStore> store) {
ui.append("MyPage.ui");
// Button click binding (static key - must start uppercase)
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#SaveButton",
com.hypixel.hytale.server.core.ui.builder.EventData.of("Action", "save")
);
// Text field binding (reference key - @ prefix reads element value)
events.addEventBinding(
CustomUIEventBindingType.ValueChanged,
"#NameInput",
com.hypixel.hytale.server.core.ui.builder.EventData.of("@NameInput", "#NameInput.Value"),
false // don't send on initial load
);
}
@Override
public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store,
EventData data) {
if ("save".equals(data.action)) {
close();
} else if (data.nameInput != null) {
// Handle text input change
}
}
// Typed event data with BuilderCodec for deserialization
public static class EventData {
public static final BuilderCodec<EventData> CODEC =
BuilderCodec.builder(EventData.class, EventData::new)
.addField(new KeyedCodec<>("Action", Codec.STRING),
(d, v) -> d.action = v, d -> d.action)
.addField(new KeyedCodec<>("NameInput", Codec.STRING),
(d, v) -> d.nameInput = v, d -> d.nameInput)
.build();
public String action;
public String nameInput;
}
}
.ui File Format
$Common = "Common.ui";
@MyTex = PatchStyle(TexturePath: "MyBackground.png");
Group {
LayoutMode: Center;
Group #Panel {
Background: @MyTex;
Anchor: (Width: 400, Height: 300);
LayoutMode: Top;
Label #Title {
Style: (FontSize: 24, Alignment: Center);
Anchor: (Top: 20, Height: 40);
Text: "Hello World";
}
TextField #NameInput {
Style: $Common.@DefaultInputFieldStyle;
Background: $Common.@InputBoxBackground;
Anchor: (Top: 10, Width: 300, Height: 50);
Padding: (Full: 10);
}
}
}
Syntax:
- $Variable = "file.ui" β import another UI file
- @Variable = PatchStyle(...) β define reusable texture
- #ElementId β unique identifier for Java access
- Element types: Group, Label, TextField
- LayoutMode: Center, Top
Item Definition (bundled in plugin)
Complete Working Example
Directory structure:
my-plugin/src/main/resources/
βββ manifest.json
βββ Server/Item/Items/
β βββ My_Item.json
βββ Common/
βββ Icons/ItemsGenerated/
β βββ My_Item.png # 64x64, 8-bit RGBA PNG
βββ Items/My_Item/
βββ My_Item.blockymodel
βββ My_Item_Texture.png # 8-bit RGBA PNG
manifest.json:
{
"Group": "com.example",
"Name": "MyPlugin",
"Version": "1.0.0",
"Main": "com.example.myplugin.MyPlugin",
"IncludesAssetPack": true,
"Dependencies": {
"Hytale:EntityModule": "*",
"Hytale:BlockModule": "*"
}
}
Server/Item/Items/My_Item.json:
{
"TranslationProperties": {
"Name": "My Custom Item",
"Description": "A custom item from my plugin"
},
"Id": "My_Item",
"Icon": "Icons/ItemsGenerated/My_Item.png",
"Model": "Items/My_Item/My_Item.blockymodel",
"Texture": "Items/My_Item/My_Item_Texture.png",
"Quality": "Common",
"MaxStack": 64,
"Categories": ["Items.Example"]
}
CRITICAL: The Id field MUST be present and match the filename. Items without Id are silently ignored.
Spawn the item: /spawnitem myplugin:My_Item
Weapon Definition
{
"Id": "Custom_Sword",
"Parent": "Template_Weapon_Sword",
"TranslationProperties": { "Name": "Custom Sword" },
"Model": "Resources/Custom_Sword/model.blockymodel",
"Texture": "Resources/Custom_Sword/texture.png",
"Icon": "Icons/ItemsGenerated/Custom_Sword.png",
"PlayerAnimationsId": "Sword",
"Quality": "Rare",
"MaxStack": 1
}
Key weapon properties:
- PlayerAnimationsId β links to player animation set
- Parent β inherit from template (reduces duplication)
- Interactions β Primary/Secondary/Ability attack chains
Build & Deploy
# Build plugin JAR
gradle -p plugin-name shadowJar
# Copy to server mods folder (IMPORTANT: server/Server/mods/, NOT server/mods/)
cp plugin-name/build/libs/*.jar server/Server/mods/
# Start server
./server/start.sh
# Verify plugin loaded (check server logs for):
# [PluginManager] - com.example:PluginName from path PluginName-1.0.0.jar
# [PluginName|P] Plugin loaded!
# Verify item registered (check logs for warning, which means it's working):
# [AssetStore|Item] Unused key(s) in 'My_Item' file /Server/Item/Items/My_Item.json: Id
Context7 Library IDs
For fetching up-to-date documentation:
/websites/hytalemodding_dev_en # Community guides (1125 snippets)
/logan-mcduffie/hytale-toolkit # Decompiled source (45103 snippets)
/websites/britakee-studios_gitbook_io_hytale-modding-documentation # Tutorials (457 snippets)
Key Packages
| Package | Purpose |
|---|---|
com.hypixel.hytale.server.core.plugin |
JavaPlugin, JavaPluginInit |
com.hypixel.hytale.server.core.command.system |
AbstractCommand, CommandContext |
com.hypixel.hytale.event |
EventRegistry |
com.hypixel.hytale.server.core.entity.entities.player.pages |
CustomUIPage, InteractiveCustomUIPage |
com.hypixel.hytale.codec.builder |
BuilderCodec |
com.hypixel.hytale.server.core.interaction |
Interaction system |
com.hypixel.hytale.protocol.packets.interface_ |
CustomUIEventBindingType, CustomPageLifetime |
Plugin Registries
| Registry | Access Method | Purpose |
|---|---|---|
logger |
getLogger() |
Plugin logging (Flogger) |
eventRegistry |
getEventRegistry() |
Event handling |
commandRegistry |
getCommandRegistry() |
Command registration |
taskRegistry |
getTaskRegistry() |
Scheduled tasks |
dataDirectory |
getDataDirectory() |
Persistent storage path |
assetRegistry |
getAssetRegistry() |
Game assets |
Decompiled Source Reference
This skill includes documentation extracted from the decompiled HytaleServer.jar (5,237 Java files, 894 packages).
Reference Files
| File | Description |
|---|---|
| reference/guides.md | Community patterns (commands, ECS, UI, inventory) |
| reference/events.md | Complete events list with registration patterns |
| reference/packets.md | Client-to-server packet reference |
| reference/decompiled.md | Navigation guide for decompiled source |
| reference/PACKAGES.md | Key packages for plugin development |
| reference/API_REFERENCE.md | Core plugin APIs |
Quick Hierarchy Reference
Plugins: PluginBase β JavaPlugin β Your plugin
Commands:
- AbstractCommand β Basic commands
- AbstractAsyncCommand β Async commands
- AbstractPlayerCommand β Player-only commands
- AbstractCommandCollection β Subcommand groups
UI Pages:
- CustomUIPage β Base page
- BasicCustomUIPage β Read-only pages
- InteractiveCustomUIPage<T> β Pages with input
Interactions:
- Interaction β Base class
- SimpleInstantInteraction β One-shot actions
- ChargingInteraction β Hold to charge
- ChainingInteraction β Combo chains
Built-in Plugin Examples
The decompiled source contains ~57 built-in plugins showing Hypixel's implementation patterns:
| Plugin | Purpose |
|---|---|
CraftingPlugin |
Recipe and crafting system |
WeatherPlugin |
Weather effects |
PortalsPlugin |
Portal mechanics |
ShopPlugin |
NPC shops |
ObjectivePlugin |
Quest objectives |
FarmingPlugin |
Farming mechanics |
Reference
For extended patterns (ECS, animations, packet handling, interactions), see patterns.md.
For decompiled source navigation, see reference/decompiled.md.
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.