Update script to write to vendor/hytale-server
This commit is contained in:
429
docs/14-early-plugin-system.md
Normal file
429
docs/14-early-plugin-system.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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:
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
1. **Minimize transformations** - Only transform what's absolutely necessary
|
||||
2. **Use priorities wisely** - Don't use high priority without good reason
|
||||
3. **Handle errors gracefully** - Return null on failure, don't crash
|
||||
4. **Log transformations** - Provide debug logging for troubleshooting
|
||||
5. **Document changes** - Clearly document what your transformer modifies
|
||||
6. **Test thoroughly** - Test with different server versions
|
||||
7. **Check compatibility** - Verify compatibility with other known early plugins
|
||||
8. **Version your transformer** - Track which server versions are supported
|
||||
9. **Provide fallbacks** - If transformation fails, the mod should degrade gracefully
|
||||
10. **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
|
||||
Reference in New Issue
Block a user