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:
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¶
- Copiar el JAR al directorio de plugins:
- Estructura final:
Ejecutar¶
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¶
Solo transformamos HytaleServer, ignorando todas las demás clases para eficiencia.
2. Lectura con ClassReader¶
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¶
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¶
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¶
- Class Transformer: Ejemplo avanzado de transformación
- Tutorial: Hello World: Tutorial paso a paso
- Early Plugins: Documentación completa
¿Preguntas? Consulta FAQ