Skip to content

Ejemplo: Plugin Simple

Este ejemplo muestra cómo crear un plugin básico que imprime un mensaje cuando el servidor inicia.

Estructura del Proyecto

simple-plugin/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/
│       │       └── ejemplo/
│       │           └── simpleplugin/
│       │               └── SimpleTransformer.java
│       └── resources/
│           └── META-INF/
│               └── services/
│                   └── com.hypixel.hytale.plugin.early.ClassTransformer
├── build.gradle
└── README.md

Código Fuente

SimpleTransformer.java

package com.ejemplo.simpleplugin;

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

/**
 * Transformador simple que inyecta un mensaje de bienvenida
 * en el constructor del servidor de Hytale.
 */
public class SimpleTransformer implements ClassTransformer {

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

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

    @Override
    @Nullable
    public byte[] transform(@Nonnull String className,
                           @Nonnull String classPath,
                           @Nonnull byte[] bytecode) {
        try {
            // Solo transformar HytaleServer
            if (!className.equals(TARGET_CLASS)) {
                return null;
            }

            System.out.println("[SimplePlugin] Transformando: " + className);
            return transformServer(bytecode);

        } catch (Exception e) {
            System.err.println("[SimplePlugin] Error: " + e.getMessage());
            e.printStackTrace();
            return null; // Retornar null en caso de error
        }
    }

    /**
     * Transforma la clase HytaleServer para inyectar nuestro mensaje.
     */
    private byte[] transformServer(byte[] bytecode) {
        ClassReader reader = new ClassReader(bytecode);
        ClassWriter writer = new ClassWriter(
            reader,
            ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS
        );

        // Visitor que modifica el bytecode
        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 MethodVisitor(Opcodes.ASM9, mv) {
                        @Override
                        public void visitCode() {
                            super.visitCode();
                            // Inyectar código al inicio del constructor
                            injectWelcomeMessage(mv);
                        }
                    };
                }

                return mv;
            }
        };

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

    /**
     * Inyecta bytecode equivalente a:
     * System.out.println("¡Simple Plugin cargado!");
     */
    private void injectWelcomeMessage(MethodVisitor mv) {
        // System.out
        mv.visitFieldInsn(
            Opcodes.GETSTATIC,
            "java/lang/System",
            "out",
            "Ljava/io/PrintStream;"
        );

        // String literal
        mv.visitLdcInsn("¡Simple Plugin cargado!");

        // println()
        mv.visitMethodInsn(
            Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream",
            "println",
            "(Ljava/lang/String;)V",
            false
        );
    }
}

Archivo de Servicio

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

com.ejemplo.simpleplugin.SimpleTransformer

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'

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

jar {
    manifest {
        attributes(
            'Implementation-Title': 'Simple Plugin',
            'Implementation-Version': version
        )
    }

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

    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

Compilar

# Compilar el plugin
./gradlew build

# El JAR estará en build/libs/
ls build/libs/
# simple-plugin-1.0.0.jar

Instalar

  1. Copiar el JAR al directorio de plugins:
cp build/libs/simple-plugin-1.0.0.jar servidor/earlyplugins/
  1. Estructura final:
servidor/
├── earlyplugins/
│   └── simple-plugin-1.0.0.jar
└── hytale-server.jar

Ejecutar

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

Salida Esperada

[EarlyPlugin] Found: simple-plugin-1.0.0.jar
[EarlyPlugin] Loading transformer: com.ejemplo.simpleplugin.SimpleTransformer (priority=100)
===============================================================================================
                              Loaded 1 class transformer(s)!!
===============================================================================================
[SimplePlugin] Transformando: com.hypixel.hytale.server.core.HytaleServer
¡Simple Plugin cargado!
[Hytale] Server starting...

Explicación del Código

1. Filtrado de Clases

if (!className.equals(TARGET_CLASS)) {
    return null;
}

Solo transformamos HytaleServer, ignorando todas las demás clases para eficiencia.

2. Lectura con ClassReader

ClassReader reader = new ClassReader(bytecode);

Lee el bytecode original de la clase.

3. Escritura con ClassWriter

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

COMPUTE_FRAMES y COMPUTE_MAXS calculan automáticamente los stack frames y tamaños de stack.

4. Visitor Pattern

ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) {
    // Interceptar métodos
};

ASM usa el patrón Visitor para recorrer y modificar el bytecode.

5. Inyección de Código

mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", ...);
mv.visitLdcInsn("¡Simple Plugin cargado!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", ...);

Genera bytecode equivalente a System.out.println("...").

Modificaciones Posibles

Cambiar el Mensaje

mv.visitLdcInsn("¡Mi mensaje personalizado!");

Inyectar al Final del Método

@Override
public void visitInsn(int opcode) {
    // Antes del RETURN
    if (opcode == Opcodes.RETURN) {
        injectMessage(mv);
    }
    super.visitInsn(opcode);
}

Transformar Múltiples Clases

private static final String[] TARGET_CLASSES = {
    "com.hypixel.hytale.server.core.HytaleServer",
    "com.hypixel.hytale.server.core.entity.LivingEntity"
};

if (!Arrays.asList(TARGET_CLASSES).contains(className)) {
    return null;
}

Siguiente


¿Preguntas? Consulta FAQ