Skip to content

Early Plugins

Los Early Plugins son plugins especiales que se cargan antes del inicio del servidor principal de Hytale, permitiendo modificaciones fundamentales al comportamiento del juego mediante transformación de bytecode.

¿Qué son los Early Plugins?

Los Early Plugins son la forma más poderosa de modificar Hytale, ya que permiten:

  • Transformar clases del juego antes de que sean cargadas
  • Inyectar código en métodos existentes
  • Modificar campos y métodos de clases
  • Cambiar el comportamiento fundamental del servidor

Diferencias con Standard Plugins

Característica Early Plugin Standard Plugin
Momento de carga Antes del servidor Después del servidor
Transformación de bytecode No
Acceso a API Limitado Completo
Requiere confirmación No
Nivel de acceso Sistema completo Solo API pública
Riesgo Alto Bajo
Complejidad Alta Media

Cuándo Usar Early Plugins

✅ Usa Early Plugins para:

  • Modificar comportamiento fundamental del servidor
  • Inyectar hooks en métodos del juego
  • Agregar funcionalidad que no existe en la API
  • Interceptar llamadas a métodos críticos
  • Modificar constantes en tiempo de carga
  • Implementar mixins o aspectos

❌ No uses Early Plugins para:

  • Funcionalidad simple que puede hacerse con API estándar
  • Almacenar datos (usa Meta System)
  • Agregar comandos (usa API de comandos)
  • Escuchar eventos (usa sistema de eventos)

Proceso de Carga

sequenceDiagram
    participant Main as Main
    participant EPL as EarlyPluginLoader
    participant FS as FileSystem
    participant UCL as URLClassLoader
    participant SL as ServiceLoader
    participant CT as ClassTransformer
    participant Confirm as Sistema de Confirmación

    Main->>EPL: loadEarlyPlugins(args)
    EPL->>FS: Escanear earlyplugins/
    FS-->>EPL: Lista de archivos .jar

    EPL->>EPL: Parsear --early-plugins
    EPL->>FS: Escanear directorios adicionales
    FS-->>EPL: Más archivos .jar

    EPL->>UCL: Crear URLClassLoader
    UCL-->>EPL: ClassLoader creado

    EPL->>SL: ServiceLoader.load(ClassTransformer.class)
    SL->>CT: Instanciar transformadores
    CT-->>SL: Instancias creadas
    SL-->>EPL: Lista de transformadores

    EPL->>EPL: Ordenar por prioridad

    alt Hay transformadores
        EPL->>Confirm: Solicitar confirmación
        Confirm-->>EPL: Usuario confirma
    end

    EPL-->>Main: Plugins cargados y listos

Estructura de un Early Plugin

Estructura de Directorio

mi-early-plugin/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/
│       │       └── ejemplo/
│       │           └── plugin/
│       │               ├── MiTransformador.java
│       │               └── helpers/
│       │                   └── ASMHelper.java
│       └── resources/
│           └── META-INF/
│               └── services/
│                   └── com.hypixel.hytale.plugin.early.ClassTransformer
├── build.gradle
└── README.md

Implementación del Transformador

package com.ejemplo.plugin;

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

public class MiTransformador implements ClassTransformer {

    // Lista de clases que queremos transformar
    private static final String[] TARGET_CLASSES = {
        "com.hypixel.hytale.server.core.HytaleServer",
        "com.hypixel.hytale.server.core.entity.LivingEntity"
    };

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

    @Override
    @Nullable
    public byte[] transform(@Nonnull String className,
                           @Nonnull String classPath,
                           @Nonnull byte[] bytecode) {
        try {
            // Verificar si es una clase que queremos transformar
            for (String target : TARGET_CLASSES) {
                if (className.equals(target)) {
                    System.out.println("[MiPlugin] Transformando: " + className);
                    return transformClass(className, bytecode);
                }
            }
        } catch (Exception e) {
            System.err.println("[MiPlugin] Error transformando " + className);
            e.printStackTrace();
        }

        // No transformar esta clase
        return null;
    }

    private byte[] transformClass(String className, byte[] bytecode) {
        ClassReader reader = new ClassReader(bytecode);
        ClassWriter writer = new ClassWriter(
            reader,
            ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
        );

        ClassVisitor visitor = createVisitor(className, writer);
        reader.accept(visitor, ClassReader.EXPAND_FRAMES);

        return writer.toByteArray();
    }

    private ClassVisitor createVisitor(String className, ClassWriter writer) {
        if (className.endsWith("HytaleServer")) {
            return new ServerTransformer(writer);
        } else if (className.endsWith("LivingEntity")) {
            return new EntityTransformer(writer);
        }
        return writer;
    }
}

Registro del Servicio

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

com.ejemplo.plugin.MiTransformador

Transformación con ASM

ASM es la biblioteca estándar para manipular bytecode Java. Los early plugins utilizan ASM para modificar clases.

Ejemplo: Inyectar Código al Inicio de un Método

public class ServerTransformer extends ClassVisitor {

    public ServerTransformer(ClassWriter writer) {
        super(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 MethodVisitor(Opcodes.ASM9, mv) {
                @Override
                public void visitCode() {
                    super.visitCode();

                    // Inyectar: System.out.println("Servidor inicializado!");
                    mv.visitFieldInsn(
                        Opcodes.GETSTATIC,
                        "java/lang/System",
                        "out",
                        "Ljava/io/PrintStream;"
                    );
                    mv.visitLdcInsn("Servidor inicializado!");
                    mv.visitMethodInsn(
                        Opcodes.INVOKEVIRTUAL,
                        "java/io/PrintStream",
                        "println",
                        "(Ljava/lang/String;)V",
                        false
                    );
                }
            };
        }

        return mv;
    }
}

Ejemplo: Modificar el Retorno de un Método

public class EntityTransformer extends ClassVisitor {

    public EntityTransformer(ClassWriter writer) {
        super(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 getMaxHealth()
        if (name.equals("getMaxHealth") && descriptor.equals("()F")) {
            return new MethodVisitor(Opcodes.ASM9, mv) {
                @Override
                public void visitInsn(int opcode) {
                    if (opcode == Opcodes.FRETURN) {
                        // Antes del RETURN, multiplicar por 2
                        mv.visitLdcInsn(2.0f);
                        mv.visitInsn(Opcodes.FMUL);
                    }
                    super.visitInsn(opcode);
                }
            };
        }

        return mv;
    }
}

Ejemplo: Agregar un Campo a una Clase

public class ClassFieldInjector extends ClassVisitor {

    private boolean fieldAdded = false;

    public ClassFieldInjector(ClassWriter writer) {
        super(Opcodes.ASM9, writer);
    }

    @Override
    public FieldVisitor visitField(int access,
                                   String name,
                                   String descriptor,
                                   String signature,
                                   Object value) {
        // Verificar si el campo ya existe
        if (name.equals("customData")) {
            fieldAdded = true;
        }
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public void visitEnd() {
        // Agregar campo si no existe
        if (!fieldAdded) {
            FieldVisitor fv = cv.visitField(
                Opcodes.ACC_PUBLIC,
                "customData",
                "Ljava/lang/String;",
                null,
                null
            );
            fv.visitEnd();
        }
        super.visitEnd();
    }
}

Configuración del Proyecto

build.gradle

plugins {
    id 'java'
}

group = 'com.ejemplo'
version = '1.0.0'

sourceCompatibility = 17
targetCompatibility = 17

repositories {
    mavenCentral()
}

dependencies {
    // ASM para transformación de bytecode
    implementation 'org.ow2.asm:asm:9.5'
    implementation 'org.ow2.asm:asm-commons:9.5'
    implementation 'org.ow2.asm:asm-tree:9.5'
    implementation 'org.ow2.asm:asm-util:9.5'

    // Hytale (provided - no incluir en el JAR)
    compileOnly files('libs/hytale-server.jar')
}

jar {
    manifest {
        attributes(
            'Implementation-Title': project.name,
            'Implementation-Version': project.version
        )
    }

    // Incluir dependencias en el JAR
    from {
        configurations.runtimeClasspath.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }

    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

pom.xml (Maven)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ejemplo</groupId>
    <artifactId>mi-early-plugin</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- ASM -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.5</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.5</version>
        </dependency>

        <!-- Hytale (provided) -->
        <dependency>
            <groupId>com.hypixel</groupId>
            <artifactId>hytale-server</artifactId>
            <version>1.0.0</version>
            <scope>provided</scope>
            <systemPath>${project.basedir}/libs/hytale-server.jar</systemPath>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Incluir dependencias -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Mejores Prácticas

✅ Hacer

  1. Validar antes de transformar

    if (bytecode == null || bytecode.length == 0) {
        return null;
    }
    

  2. Usar COMPUTE_FRAMES

    ClassWriter writer = new ClassWriter(
        reader,
        ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
    );
    

  3. Logging detallado

    System.out.println("[Plugin] Transformando: " + className);
    System.out.println("[Plugin] Inyectando en método: " + methodName);
    

  4. Manejo de errores robusto

    try {
        return transform(bytecode);
    } catch (Exception e) {
        System.err.println("[Plugin] Error: " + e.getMessage());
        return null; // Retornar original
    }
    

  5. Documentar transformaciones

    /**
     * Transforma HytaleServer para inyectar inicialización
     * personalizada en el constructor.
     */
    

❌ No Hacer

  1. No modificar sin verificar

    // MAL: Transformar sin verificar
    return transform(bytecode);
    
    // BIEN: Verificar primero
    if (shouldTransform(className)) {
        return transform(bytecode);
    }
    return null;
    

  2. No asumir estructura de clase

    // MAL: Asumir que el método existe
    if (name.equals("update")) { ... }
    
    // BIEN: Verificar descriptor también
    if (name.equals("update") && descriptor.equals("(F)V")) { ... }
    

  3. No transformar clases del sistema

    // MAL: Transformar clases de Java
    if (className.startsWith("java.")) {
        return transform(bytecode);
    }
    
    // BIEN: Solo transformar clases de Hytale
    if (className.startsWith("com.hypixel.hytale.")) {
        return transform(bytecode);
    }
    

Depuración de Early Plugins

Activar Verificación de Bytecode

ClassReader reader = new ClassReader(bytecode);
ClassWriter writer = new ClassWriter(
    reader,
    ClassWriter.COMPUTE_FRAMES
);

// Agregar verificador
ClassVisitor verifier = new CheckClassAdapter(writer);
reader.accept(verifier, 0);

Imprimir Bytecode Transformado

byte[] transformed = transform(bytecode);

// Imprimir bytecode legible
ClassReader reader = new ClassReader(transformed);
TraceClassVisitor tracer = new TraceClassVisitor(
    new PrintWriter(System.out)
);
reader.accept(tracer, 0);

return transformed;

Comparar Antes y Después

System.out.println("=== ANTES ===");
printBytecode(bytecode);

byte[] transformed = transform(bytecode);

System.out.println("=== DESPUÉS ===");
printBytecode(transformed);

Limitaciones y Consideraciones

Limitaciones Técnicas

  • Solo se pueden transformar clases que aún no están cargadas
  • No se puede des-transformar una clase
  • Las transformaciones son permanentes durante la sesión
  • El bytecode inválido causa VerifyError

Consideraciones de Rendimiento

  • La transformación ocurre durante la carga de clase
  • Múltiples transformadores afectan el tiempo de inicio
  • Optimizar el filtrado de clases
  • Evitar operaciones costosas en transform()

Consideraciones de Compatibilidad

  • Las actualizaciones de Hytale pueden romper transformaciones
  • Verificar compatibilidad con cada versión
  • Probar exhaustivamente antes de desplegar
  • Mantener documentación de transformaciones

Seguridad

Riesgo de Seguridad

Los early plugins tienen acceso completo al sistema. Solo usa plugins de fuentes 100% confiables.

Riesgos

  • Ejecución de código arbitrario
  • Acceso al sistema de archivos
  • Modificación de memoria
  • Escalada de privilegios

Mitigación

  1. Revisar código fuente antes de usar
  2. Usar solo plugins de confianza
  3. Ejecutar en entorno aislado (sandbox/VM)
  4. Monitorear comportamiento del servidor
  5. Hacer backups antes de instalar plugins

Siguiente


¿Preguntas? Consulta FAQ o Troubleshooting