Creando tu Primer Plugin de Hytale¶
Esta guía te llevará paso a paso por el proceso de crear un plugin completo para Hytale, desde la configuración inicial hasta la implementación y pruebas.
Requisitos Previos¶
Antes de empezar, asegúrate de tener:
- ✅ JDK 17+ instalado
- ✅ IntelliJ IDEA o tu IDE preferido
- ✅ Gradle o Maven configurado
- ✅ Conocimientos básicos de Java
- ✅ Servidor de Hytale para pruebas
Tipos de Plugins¶
Early Plugin¶
Plugins que se cargan antes del servidor y pueden transformar bytecode.
Casos de uso: - Modificar comportamiento del núcleo del juego - Inyectar código en clases existentes - Agregar funcionalidad a nivel bajo
Standard Plugin¶
Plugins que se cargan después del servidor usando la API completa.
Casos de uso: - Comandos personalizados - Nuevas interacciones - Sistemas de juego adicionales
Proyecto 1: Early Plugin Simple¶
Vamos a crear un plugin que registre cuando el servidor se inicia.
Paso 1: Crear Estructura del Proyecto¶
Estructura:
mi-primer-plugin/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── miusuario/
│ │ └── miplugin/
│ │ └── MiTransformador.java
│ └── resources/
│ └── META-INF/
│ └── services/
│ └── com.hypixel.hytale.plugin.early.ClassTransformer
├── build.gradle
└── settings.gradle
Paso 2: Configurar build.gradle¶
plugins {
id 'java'
}
group = 'com.miusuario.miplugin'
version = '1.0.0'
sourceCompatibility = '17'
targetCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
// API de Hytale (descompilada)
compileOnly files('libs/hytale-api.jar')
// ASM para transformación de bytecode
implementation 'org.ow2.asm:asm:9.6'
implementation 'org.ow2.asm:asm-commons:9.6'
implementation 'org.ow2.asm:asm-util:9.6'
// FastUtil (usado por Hytale)
compileOnly 'it.unimi.dsi:fastutil:8.5.12'
}
jar {
manifest {
attributes(
'Implementation-Title': project.name,
'Implementation-Version': project.version,
'Main-Class': 'com.miusuario.miplugin.MiTransformador'
)
}
// Incluir dependencias en el JAR
from {
configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
Paso 3: Crear el Transformador¶
src/main/java/com/miusuario/miplugin/MiTransformador.java:
package com.miusuario.miplugin;
import com.hypixel.hytale.plugin.early.ClassTransformer;
import org.objectweb.asm.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Mi primer transformador de Hytale!
*
* Este transformador intercepta el constructor de HytaleServer
* y agrega un mensaje de bienvenida.
*/
public class MiTransformador implements ClassTransformer {
private static final String TARGET_CLASS =
"com.hypixel.hytale.server.core.HytaleServer";
@Override
public int priority() {
// Prioridad media
return 50;
}
@Override
@Nullable
public byte[] transform(@Nonnull String className,
@Nonnull String classPath,
@Nonnull byte[] bytecode) {
try {
// Solo transformar la clase objetivo
if (className.equals(TARGET_CLASS)) {
System.out.println("[MiPlugin] Transformando HytaleServer...");
return transformarServidor(bytecode);
}
} catch (Exception e) {
System.err.println("[MiPlugin] Error: " + e.getMessage());
e.printStackTrace();
}
// No modificar otras clases
return null;
}
/**
* Transforma la clase HytaleServer usando ASM.
*/
private byte[] transformarServidor(byte[] bytecode) {
ClassReader reader = new ClassReader(bytecode);
ClassWriter writer = new ClassWriter(
reader,
ClassWriter.COMPUTE_FRAMES
);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access,
String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(
access, name, descriptor, signature, exceptions
);
// Interceptar el constructor
if (name.equals("<init>")) {
return new ConstructorVisitor(Opcodes.ASM9, mv);
}
return mv;
}
};
reader.accept(visitor, 0);
return writer.toByteArray();
}
/**
* MethodVisitor que modifica el constructor.
*/
private static class ConstructorVisitor extends MethodVisitor {
public ConstructorVisitor(int api, MethodVisitor mv) {
super(api, mv);
}
@Override
public void visitCode() {
super.visitCode();
// Inyectar: System.out.println("¡Mi Plugin está activo!")
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn("========================================");
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn(" ¡Mi Plugin está activo!");
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn(" Versión: 1.0.0");
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn("========================================");
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
}
}
}
Paso 4: Registrar el Service Loader¶
src/main/resources/META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer:
Importante
Este archivo NO debe tener extensión. El nombre completo es exactamente:
com.hypixel.hytale.plugin.early.ClassTransformer
Paso 5: Compilar el Plugin¶
Paso 6: Instalar en el Servidor¶
# Copiar al directorio de early plugins
cp build/libs/mi-primer-plugin-1.0.0.jar /ruta/servidor/earlyplugins/
Paso 7: Ejecutar el Servidor¶
Salida esperada:
[EarlyPlugin] Found: mi-primer-plugin-1.0.0.jar
[EarlyPlugin] Loading transformer: com.miusuario.miplugin.MiTransformador (priority=50)
===============================================================================================
Loaded 1 class transformer(s)!!
===============================================================================================
[MiPlugin] Transformando HytaleServer...
========================================
¡Mi Plugin está activo!
Versión: 1.0.0
========================================
INFO - HytaleServer starting...
Proyecto 2: Plugin con Configuración¶
Ahora vamos a crear un plugin más avanzado con archivo de configuración.
Estructura del Proyecto¶
plugin-avanzado/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/miusuario/avanzado/
│ │ ├── PluginAvanzado.java
│ │ ├── ConfigManager.java
│ │ └── transformers/
│ │ └── ServerTransformer.java
│ └── resources/
│ ├── META-INF/services/...
│ └── config.yml (template)
└── build.gradle
ConfigManager.java¶
package com.miusuario.avanzado;
import java.io.*;
import java.nio.file.*;
import java.util.Properties;
public class ConfigManager {
private final Path configPath;
private final Properties config;
public ConfigManager(String configFile) {
this.configPath = Paths.get(configFile);
this.config = new Properties();
loadConfig();
}
private void loadConfig() {
if (!Files.exists(configPath)) {
createDefaultConfig();
}
try (InputStream in = Files.newInputStream(configPath)) {
config.load(in);
System.out.println("[PluginAvanzado] Configuración cargada");
} catch (IOException e) {
System.err.println("[PluginAvanzado] Error cargando config: "
+ e.getMessage());
}
}
private void createDefaultConfig() {
config.setProperty("plugin.enabled", "true");
config.setProperty("plugin.debug", "false");
config.setProperty("plugin.welcome-message", "¡Plugin Avanzado activo!");
try {
Files.createDirectories(configPath.getParent());
try (OutputStream out = Files.newOutputStream(configPath)) {
config.store(out, "Plugin Avanzado - Configuración");
}
System.out.println("[PluginAvanzado] Config creada en: " + configPath);
} catch (IOException e) {
System.err.println("[PluginAvanzado] Error creando config: "
+ e.getMessage());
}
}
public String getString(String key, String defaultValue) {
return config.getProperty(key, defaultValue);
}
public boolean getBoolean(String key, boolean defaultValue) {
return Boolean.parseBoolean(
config.getProperty(key, String.valueOf(defaultValue))
);
}
public int getInt(String key, int defaultValue) {
try {
return Integer.parseInt(config.getProperty(key));
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
PluginAvanzado.java¶
package com.miusuario.avanzado;
import com.hypixel.hytale.plugin.early.ClassTransformer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class PluginAvanzado implements ClassTransformer {
private static ConfigManager config;
private boolean enabled;
private boolean debug;
public PluginAvanzado() {
// Cargar configuración
config = new ConfigManager("config/plugin-avanzado.properties");
enabled = config.getBoolean("plugin.enabled", true);
debug = config.getBoolean("plugin.debug", false);
if (debug) {
System.out.println("[PluginAvanzado] Modo debug activado");
}
}
@Override
public int priority() {
return 100;
}
@Override
@Nullable
public byte[] transform(@Nonnull String className,
@Nonnull String classPath,
@Nonnull byte[] bytecode) {
if (!enabled) {
return null;
}
if (debug) {
System.out.println("[PluginAvanzado] Analizando: " + className);
}
// Tu lógica de transformación aquí
return null;
}
public static ConfigManager getConfig() {
return config;
}
}
Mejores Prácticas¶
✅ DO - Hacer¶
-
Verificar nombres de clase antes de transformar
-
Manejar excepciones apropiadamente
-
Usar logging apropiado
-
Documentar tu código
-
Testear exhaustivamente
- Prueba en servidor de desarrollo
- Verifica compatibilidad
- Prueba casos extremos
❌ DON'T - No Hacer¶
-
No transformar todo
-
No ignorar errores
-
No hardcodear valores
Debugging¶
Técnica 1: Logging Detallado¶
@Override
public byte[] transform(...) {
System.out.println("[DEBUG] Clase: " + className);
System.out.println("[DEBUG] Tamaño bytecode: " + bytecode.length);
byte[] result = doTransform(bytecode);
if (result != null) {
System.out.println("[DEBUG] Transformación exitosa!");
}
return result;
}
Técnica 2: Verificar Bytecode con ASM¶
import org.objectweb.asm.util.CheckClassAdapter;
// Verificar bytecode transformado
ClassReader cr = new ClassReader(transformedBytecode);
ClassWriter cw = new ClassWriter(0);
CheckClassAdapter ca = new CheckClassAdapter(cw);
cr.accept(ca, 0); // Lanza excepción si hay errores
Técnica 3: Guardar Clases Transformadas¶
private void saveTransformedClass(String className, byte[] bytecode) {
try {
Path output = Paths.get("debug", className + ".class");
Files.createDirectories(output.getParent());
Files.write(output, bytecode);
System.out.println("[DEBUG] Clase guardada: " + output);
} catch (IOException e) {
e.printStackTrace();
}
}
Próximos Pasos¶
- Class Transformation: Técnicas avanzadas
- Bytecode Manipulation: Guía completa de ASM
- Best Practices: Mejores prácticas detalladas
Recursos¶
¿Problemas? Consulta Troubleshooting