12 KiB
12 KiB
Early Plugin System (Class Transformation)
The Early Plugin System allows advanced mods to transform Java bytecode before classes are loaded. This is a powerful feature for core modifications that cannot be achieved through the standard plugin API.
Overview
Early plugins are loaded before the main server initialization and can:
- Transform class bytecode
- Modify method implementations
- Add fields or methods to existing classes
- Inject custom behavior into core systems
Warning: This is an advanced feature. Incorrect use can break the server or cause incompatibilities with other mods.
When to Use Early Plugins
Early plugins are appropriate when:
- You need to modify core server behavior not exposed through APIs
- You need to patch bugs or security issues
- You're implementing advanced hooks not available through events
- Standard plugin APIs don't provide sufficient access
Prefer standard plugins when possible. Early plugins:
- Are harder to maintain across server updates
- May conflict with other early plugins
- Can introduce hard-to-debug issues
Creating an Early Plugin
Project Structure
my-early-plugin/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/
│ │ └── MyTransformer.java
│ └── resources/
│ └── META-INF/
│ └── services/
│ └── com.hypixel.hytale.plugin.early.ClassTransformer
└── build.gradle
Implementing ClassTransformer
package com.example;
import com.hypixel.hytale.plugin.early.ClassTransformer;
public class MyTransformer implements ClassTransformer {
@Override
public int priority() {
// Higher priority = loaded first
// Use 0 for normal priority
return 0;
}
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Return null to skip transformation
// Return modified bytes to transform
// Only transform specific classes
if (!className.equals("com/hypixel/hytale/server/core/SomeClass")) {
return null;
}
// Use ASM or similar library to transform bytecode
return transformClass(classBytes);
}
private byte[] transformClass(byte[] classBytes) {
// Bytecode transformation logic
// Use ASM, Javassist, etc.
return classBytes;
}
}
Service Registration
Create META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer:
com.example.MyTransformer
Deployment
Place the compiled JAR in the server's earlyplugins/ directory.
Bytecode Transformation with ASM
Basic ASM Example
import org.objectweb.asm.*;
public class MyTransformer implements ClassTransformer {
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
if (!className.equals("com/hypixel/hytale/target/TargetClass")) {
return null;
}
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new MyClassVisitor(writer);
reader.accept(visitor, 0);
return writer.toByteArray();
}
}
class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("targetMethod")) {
return new MyMethodVisitor(mv);
}
return mv;
}
}
class MyMethodVisitor extends MethodVisitor {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitCode();
// Inject code at method start
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("Method called!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
}
Adding Method Hooks
class HookMethodVisitor extends MethodVisitor {
private final String hookClass;
private final String hookMethod;
public HookMethodVisitor(MethodVisitor mv, String hookClass, String hookMethod) {
super(Opcodes.ASM9, mv);
this.hookClass = hookClass;
this.hookMethod = hookMethod;
}
@Override
public void visitCode() {
super.visitCode();
// Call hook at method start
mv.visitMethodInsn(Opcodes.INVOKESTATIC, hookClass, hookMethod, "()V", false);
}
@Override
public void visitInsn(int opcode) {
// Inject before return
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, hookClass, hookMethod + "End", "()V", false);
}
super.visitInsn(opcode);
}
}
Modifying Method Parameters
class ParameterModifierVisitor extends MethodVisitor {
public ParameterModifierVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitCode();
// Modify first parameter (index 1 for instance methods, 0 for static)
// Load parameter, modify, store back
mv.visitVarInsn(Opcodes.ALOAD, 1); // Load first object parameter
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/Hooks", "modifyParam",
"(Ljava/lang/Object;)Ljava/lang/Object;", false);
mv.visitVarInsn(Opcodes.ASTORE, 1); // Store modified value
}
}
Priority System
Transformer priority determines load order:
@Override
public int priority() {
return 100; // Higher = loaded first
}
| Priority | Use Case |
|---|---|
| 1000+ | Critical patches (security fixes) |
| 100-999 | Core modifications |
| 0 | Standard transformations |
| -100 to -1 | Post-processing |
Compatibility Considerations
Class Name Handling
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// className: Internal name (com/example/MyClass)
// transformedName: May differ if class was renamed
// Always use transformedName for matching
if (!transformedName.equals("com.hypixel.hytale.target.TargetClass")) {
return null;
}
return transformBytes(classBytes);
}
Version Checking
public class MyTransformer implements ClassTransformer {
private static final String TARGET_VERSION = "1.0.0";
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Check server version compatibility
if (!isCompatibleVersion()) {
System.err.println("MyTransformer incompatible with this server version");
return null;
}
return doTransform(classBytes);
}
private boolean isCompatibleVersion() {
// Check against known compatible versions
return true;
}
}
Chaining with Other Transformers
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
// Be careful not to break other transformers
// Avoid removing methods/fields other transformers may depend on
// Add, don't remove when possible
return safeTransform(classBytes);
}
Debugging Early Plugins
Logging
public class MyTransformer implements ClassTransformer {
private static final boolean DEBUG = Boolean.getBoolean("mymod.debug");
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
if (DEBUG) {
System.out.println("[MyTransformer] Processing: " + transformedName);
}
byte[] result = doTransform(classBytes);
if (DEBUG && result != null) {
System.out.println("[MyTransformer] Transformed: " + transformedName);
}
return result;
}
}
Bytecode Verification
private byte[] transformWithVerification(byte[] classBytes) {
try {
byte[] transformed = doTransform(classBytes);
// Verify bytecode is valid
ClassReader verifyReader = new ClassReader(transformed);
ClassWriter verifyWriter = new ClassWriter(0);
verifyReader.accept(new CheckClassAdapter(verifyWriter), 0);
return transformed;
} catch (Exception e) {
System.err.println("Transformation produced invalid bytecode: " + e.getMessage());
return null; // Return null to skip transformation
}
}
Dumping Transformed Classes
@Override
public byte[] transform(String className, String transformedName, byte[] classBytes) {
byte[] result = doTransform(classBytes);
if (result != null && Boolean.getBoolean("mymod.dumpClasses")) {
try {
Path dumpPath = Path.of("transformed", transformedName.replace('.', '/') + ".class");
Files.createDirectories(dumpPath.getParent());
Files.write(dumpPath, result);
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
Common Transformation Patterns
Adding a Field
class AddFieldVisitor extends ClassVisitor {
public AddFieldVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public void visitEnd() {
// Add field before class end
FieldVisitor fv = cv.visitField(
Opcodes.ACC_PUBLIC,
"myCustomField",
"Ljava/lang/Object;",
null,
null
);
if (fv != null) {
fv.visitEnd();
}
super.visitEnd();
}
}
Redirecting Method Calls
class RedirectMethodVisitor extends MethodVisitor {
public RedirectMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
// Redirect specific method call
if (owner.equals("com/hypixel/hytale/Original") && name.equals("oldMethod")) {
super.visitMethodInsn(opcode, "com/example/Replacement", "newMethod",
descriptor, isInterface);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
Best Practices
- Minimize transformations - Only transform what's absolutely necessary
- Use priorities wisely - Don't use high priority without good reason
- Handle errors gracefully - Return null on failure, don't crash
- Log transformations - Provide debug logging for troubleshooting
- Document changes - Clearly document what your transformer modifies
- Test thoroughly - Test with different server versions
- Check compatibility - Verify compatibility with other known early plugins
- Version your transformer - Track which server versions are supported
- Provide fallbacks - If transformation fails, the mod should degrade gracefully
- Keep it simple - Complex transformations are hard to maintain
Security Considerations
- Early plugins have full access to server internals
- Malicious early plugins could compromise server security
- Only use early plugins from trusted sources
- Review transformer code before deployment
- Monitor for unexpected behavior after installing early plugins