Aller au contenu

Plugin API - Visión General

La Plugin API de Hytale permite crear extensiones que modifican el comportamiento del juego mediante transformación de bytecode en tiempo de carga.

Paquete Principal

package com.hypixel.hytale.plugin.early;

Componentes

ClassTransformer

Interfaz principal para transformar bytecode de clases Java.

Archivo: ClassTransformer.java:6

public interface ClassTransformer {
    /**
     * Prioridad del transformador.
     * Mayor prioridad = se ejecuta primero.
     *
     * @return prioridad (default: 0)
     */
    default int priority() {
        return 0;
    }

    /**
     * Transforma el bytecode de una clase.
     *
     * @param className Nombre de la clase (com.example.MyClass)
     * @param classPath Ruta de la clase (com/example/MyClass)
     * @param bytecode Bytecode original
     * @return Bytecode transformado, o null para no modificar
     */
    @Nullable
    byte[] transform(@Nonnull String className,
                     @Nonnull String classPath,
                     @Nonnull byte[] bytecode);
}

Uso:

public class MiTransformador implements ClassTransformer {

    @Override
    public int priority() {
        return 100; // Alta prioridad
    }

    @Override
    public byte[] transform(String className,
                           String classPath,
                           byte[] bytecode) {
        if (className.equals("com.hypixel.hytale.server.core.HytaleServer")) {
            // Transformar usando ASM
            return transformWithASM(bytecode);
        }
        return null; // No modificar otras clases
    }
}

EarlyPluginLoader

Cargador de plugins que se ejecuta antes del servidor principal.

Archivo: EarlyPluginLoader.java:20

public final class EarlyPluginLoader {
    @Nonnull
    public static final Path EARLY_PLUGINS_PATH =
        Path.of("earlyplugins", new String[0]);

    /**
     * Carga early plugins desde el directorio configurado.
     *
     * @param args Argumentos del programa
     */
    public static void loadEarlyPlugins(@Nonnull String[] args);

    /**
     * Verifica si hay transformadores cargados.
     *
     * @return true si hay transformadores
     */
    public static boolean hasTransformers();

    /**
     * Obtiene la lista de transformadores cargados.
     *
     * @return Lista inmutable de transformadores
     */
    public static List<ClassTransformer> getTransformers();

    /**
     * Obtiene el ClassLoader de plugins.
     *
     * @return ClassLoader o null si no hay plugins
     */
    @Nullable
    public static URLClassLoader getPluginClassLoader();
}

Comportamiento:

  1. Escaneo de Directorios:
  2. Escanea earlyplugins/ por defecto
  3. También acepta --early-plugins=/ruta/adicional

  4. Carga de JARs:

  5. Busca todos los archivos *.jar
  6. Crea un URLClassLoader con todos los JARs

  7. Service Loader:

  8. Usa ServiceLoader para encontrar ClassTransformer
  9. Ordena por prioridad (mayor a menor)

  10. Confirmación:

  11. Requiere --accept-early-plugins o confirmación interactiva
  12. No requerido en modo --singleplayer

Código interno (EarlyPluginLoader.java:31):

public static void loadEarlyPlugins(@Nonnull String[] args) {
    ObjectArrayList<URL> urls = new ObjectArrayList<>();
    collectPluginJars(EARLY_PLUGINS_PATH, urls);

    // Parsear rutas adicionales de --early-plugins
    Iterator var2 = parseEarlyPluginPaths(args).iterator();
    while(var2.hasNext()) {
        Path path = (Path)var2.next();
        collectPluginJars(path, urls);
    }

    if (!urls.isEmpty()) {
        pluginClassLoader = new URLClassLoader(
            urls.toArray(new URL[0]),
            EarlyPluginLoader.class.getClassLoader()
        );

        // Cargar transformadores via ServiceLoader
        var2 = ServiceLoader.load(
            ClassTransformer.class,
            pluginClassLoader
        ).iterator();

        while(var2.hasNext()) {
            ClassTransformer transformer = var2.next();
            System.out.println(
                "[EarlyPlugin] Loading transformer: " +
                transformer.getClass().getName() +
                " (priority=" + transformer.priority() + ")"
            );
            transformers.add(transformer);
        }

        // Ordenar por prioridad (descendente)
        transformers.sort(
            Comparator.comparingInt(ClassTransformer::priority)
                      .reversed()
        );
    }
}

TransformingClassLoader

ClassLoader que aplica transformaciones a las clases durante su carga.

Archivo: TransformingClassLoader.java

public class TransformingClassLoader extends ClassLoader {

    /**
     * Carga una clase aplicando transformaciones.
     *
     * @param name Nombre de la clase
     * @return Clase cargada y transformada
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {

        // 1. Verificar si ya está cargada
        Class<?> c = findLoadedClass(name);
        if (c != null) {
            return c;
        }

        // 2. Leer bytecode original
        byte[] bytecode = readClassBytes(name);

        // 3. Aplicar transformadores
        for (ClassTransformer transformer :
             EarlyPluginLoader.getTransformers()) {

            byte[] transformed = transformer.transform(
                name,
                name.replace('.', '/'),
                bytecode
            );

            if (transformed != null) {
                bytecode = transformed;
            }
        }

        // 4. Definir clase con bytecode transformado
        return defineClass(name, bytecode, 0, bytecode.length);
    }
}

Flujo de Transformación

sequenceDiagram
    participant Main
    participant EPL as EarlyPluginLoader
    participant TCL as TransformingClassLoader
    participant CT as ClassTransformer

    Main->>EPL: loadEarlyPlugins(args)
    EPL->>EPL: Escanear earlyplugins/
    EPL->>EPL: Cargar transformadores
    EPL->>EPL: Ordenar por prioridad

    Note over TCL: Cuando se necesita cargar una clase...

    TCL->>TCL: readClassBytes()
    loop Para cada transformador
        TCL->>CT: transform(className, classPath, bytecode)
        CT-->>TCL: bytecode transformado (o null)
    end
    TCL->>TCL: defineClass(transformedBytecode)

Registro con Service Loader

Para que tu ClassTransformer sea detectado, debes registrarlo:

1. Crear archivo de servicio

src/main/resources/META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer:

com.ejemplo.miplugin.MiTransformador
com.ejemplo.miplugin.OtroTransformador

2. Compilar en JAR

El JAR debe contener:

mi-plugin.jar
├── com/ejemplo/miplugin/
│   ├── MiTransformador.class
│   └── OtroTransformador.class
└── META-INF/
    └── services/
        └── com.hypixel.hytale.plugin.early.ClassTransformer

3. Colocar en earlyplugins/

servidor/
├── earlyplugins/
│   └── mi-plugin.jar
└── hytale-server.jar

Prioridades

Los transformadores se ejecutan en orden de mayor a menor prioridad:

// Prioridad: 1000 - se ejecuta primero
public class TransformadorCritico implements ClassTransformer {
    @Override
    public int priority() { return 1000; }
}

// Prioridad: 100
public class TransformadorMedio implements ClassTransformer {
    @Override
    public int priority() { return 100; }
}

// Prioridad: 0 (default) - se ejecuta último
public class TransformadorNormal implements ClassTransformer {
    // priority() no implementado, usa default (0)
}

Orden de ejecución: 1. TransformadorCritico (1000) 2. TransformadorMedio (100) 3. TransformadorNormal (0)

Mejores Prácticas

✅ Hacer

  • Retornar null si no modificas la clase
  • Verificar nombre de clase antes de transformar
  • Manejar excepciones en transform()
  • Usar prioridades apropiadas
  • Documentar transformaciones

❌ No Hacer

  • No modificar todas las clases sin filtrar
  • No lanzar excepciones desde transform()
  • No hacer I/O en transform()
  • No asumir orden de transformadores con misma prioridad
  • No modificar bytecode inválido

Ejemplo Completo

package com.ejemplo.miplugin;

import com.hypixel.hytale.plugin.early.ClassTransformer;
import org.objectweb.asm.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class MiTransformador implements ClassTransformer {

    private static final String TARGET_CLASS =
        "com.hypixel.hytale.server.core.HytaleServer";

    @Override
    public int priority() {
        return 100;
    }

    @Override
    @Nullable
    public byte[] transform(@Nonnull String className,
                           @Nonnull String classPath,
                           @Nonnull byte[] bytecode) {
        try {
            if (className.equals(TARGET_CLASS)) {
                return transformarServidor(bytecode);
            }
        } catch (Exception e) {
            System.err.println(
                "[MiPlugin] Error transformando " +
                className + ": " + e.getMessage()
            );
        }
        return null;
    }

    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 método constructor
                if (name.equals("<init>")) {
                    return new MethodVisitor(Opcodes.ASM9, mv) {
                        @Override
                        public void visitCode() {
                            super.visitCode();
                            // Inyectar código al inicio
                            mv.visitFieldInsn(
                                Opcodes.GETSTATIC,
                                "java/lang/System",
                                "out",
                                "Ljava/io/PrintStream;"
                            );
                            mv.visitLdcInsn(
                                "[MiPlugin] Servidor inicializado!"
                            );
                            mv.visitMethodInsn(
                                Opcodes.INVOKEVIRTUAL,
                                "java/io/PrintStream",
                                "println",
                                "(Ljava/lang/String;)V",
                                false
                            );
                        }
                    };
                }
                return mv;
            }
        };

        reader.accept(visitor, 0);
        return writer.toByteArray();
    }
}

Siguiente


¿Preguntas? Consulta FAQ o Troubleshooting