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¶
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:
- Escaneo de Directorios:
- Escanea
earlyplugins/por defecto -
También acepta
--early-plugins=/ruta/adicional -
Carga de JARs:
- Busca todos los archivos
*.jar -
Crea un URLClassLoader con todos los JARs
-
Service Loader:
- Usa
ServiceLoaderpara encontrarClassTransformer -
Ordena por prioridad (mayor a menor)
-
Confirmación:
- Requiere
--accept-early-pluginso confirmación interactiva - 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:
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/¶
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¶
- ClassTransformer: Detalles de la interfaz
- EarlyPluginLoader: Carga de plugins
- Bytecode Manipulation: Guía de ASM
¿Preguntas? Consulta FAQ o Troubleshooting