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 | Sí | No |
| Acceso a API | Limitado | Completo |
| Requiere confirmación | Sí | 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:
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¶
-
Validar antes de transformar
-
Usar COMPUTE_FRAMES
-
Logging detallado
-
Manejo de errores robusto
-
Documentar transformaciones
❌ No Hacer¶
-
No modificar sin verificar
-
No asumir estructura de clase
-
No transformar clases del sistema
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¶
- Revisar código fuente antes de usar
- Usar solo plugins de confianza
- Ejecutar en entorno aislado (sandbox/VM)
- Monitorear comportamiento del servidor
- Hacer backups antes de instalar plugins
Siguiente¶
- ClassLoader: Sistema de carga de clases
- Ejemplo: Class Transformer: Ejemplo completo
- Tutorial: Hello World: Tutorial paso a paso
¿Preguntas? Consulta FAQ o Troubleshooting