Skip to content

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

mkdir mi-primer-plugin
cd mi-primer-plugin

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:

com.miusuario.miplugin.MiTransformador

Importante

Este archivo NO debe tener extensión. El nombre completo es exactamente: com.hypixel.hytale.plugin.early.ClassTransformer

Paso 5: Compilar el Plugin

# Con Gradle
./gradlew clean build

# El JAR estará en: build/libs/mi-primer-plugin-1.0.0.jar

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

cd /ruta/servidor
java -jar hytale-server.jar --accept-early-plugins

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

  1. Verificar nombres de clase antes de transformar

    if (className.equals("com.hypixel.hytale.specific.Class")) {
        return transform(bytecode);
    }
    

  2. Manejar excepciones apropiadamente

    try {
        return transform(bytecode);
    } catch (Exception e) {
        System.err.println("Error: " + e.getMessage());
        return null; // No modificar en caso de error
    }
    

  3. Usar logging apropiado

    System.out.println("[MiPlugin] Información importante");
    System.err.println("[MiPlugin] ERROR: Algo salió mal");
    

  4. Documentar tu código

    /**
     * Transforma la clase objetivo para agregar funcionalidad X.
     *
     * @param bytecode Bytecode original
     * @return Bytecode transformado
     */
    

  5. Testear exhaustivamente

  6. Prueba en servidor de desarrollo
  7. Verifica compatibilidad
  8. Prueba casos extremos

❌ DON'T - No Hacer

  1. No transformar todo

    // ❌ MAL
    public byte[] transform(...) {
        return transform(bytecode); // Transforma TODO
    }
    
    // ✅ BIEN
    public byte[] transform(...) {
        if (className.equals(TARGET)) {
            return transform(bytecode);
        }
        return null;
    }
    

  2. No ignorar errores

    // ❌ MAL
    try {
        transform(bytecode);
    } catch (Exception e) {
        // Ignorado silenciosamente
    }
    

  3. No hardcodear valores

    // ❌ MAL
    String message = "Hello World";
    
    // ✅ BIEN
    String message = config.getString("message", "Hello World");
    

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

Recursos


¿Problemas? Consulta Troubleshooting