Mejores Prácticas para Desarrollo de Plugins¶
Esta guía exhaustiva cubre las mejores prácticas para desarrollar plugins robustos, eficientes y mantenibles para Hytale. Incluye patrones recomendados, antipatrones a evitar, y estrategias de optimización.
Principios Fundamentales¶
1. KISS (Keep It Simple, Stupid)¶
Mantén tu código simple y fácil de entender.
// ❌ MAL - Demasiado complejo
public class ComplexTransformer implements ClassTransformer {
private Map<String, List<Tuple<MethodVisitor, Function<byte[], byte[]>>>> transformers;
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
return transformers.getOrDefault(className, Collections.emptyList())
.stream()
.reduce(bytecode, (acc, tuple) -> tuple.getSecond().apply(acc), (a, b) -> b);
}
}
// ✅ BIEN - Simple y claro
public class SimpleTransformer implements ClassTransformer {
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
if (!shouldTransform(className)) {
return null;
}
try {
return doTransform(bytecode);
} catch (Exception e) {
logError("Error transforming " + className, e);
return null;
}
}
private boolean shouldTransform(String className) {
return className.equals("com.hypixel.hytale.server.core.HytaleServer");
}
private byte[] doTransform(byte[] bytecode) {
// Lógica clara de transformación
ClassReader reader = new ClassReader(bytecode);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new MyVisitor(Opcodes.ASM9, writer);
reader.accept(visitor, 0);
return writer.toByteArray();
}
}
2. DRY (Don't Repeat Yourself)¶
Evita duplicar código.
// ❌ MAL - Código duplicado
public class DuplicatedCodeTransformer extends MethodVisitor {
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Start of method");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("End of method");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
// ✅ BIEN - Código reutilizable
public class CleanCodeTransformer extends MethodVisitor {
@Override
public void visitCode() {
super.visitCode();
injectPrintln("Start of method");
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
injectPrintln("End of method");
}
super.visitInsn(opcode);
}
/**
* Helper para inyectar System.out.println
*/
private void injectPrintln(String message) {
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;"
);
mv.visitLdcInsn(message);
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
);
}
}
3. Fail Fast¶
Detecta y reporta errores tempranamente.
// ✅ BIEN - Validación temprana
public class ValidatingTransformer implements ClassTransformer {
private final String targetClass;
private final String targetMethod;
public ValidatingTransformer(String targetClass, String targetMethod) {
// Validar en el constructor
if (targetClass == null || targetClass.isEmpty()) {
throw new IllegalArgumentException("targetClass cannot be null or empty");
}
if (targetMethod == null || targetMethod.isEmpty()) {
throw new IllegalArgumentException("targetMethod cannot be null or empty");
}
this.targetClass = targetClass;
this.targetMethod = targetMethod;
}
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// Validar parámetros
if (className == null || bytecode == null) {
throw new IllegalArgumentException("className and bytecode must not be null");
}
if (!className.equals(targetClass)) {
return null;
}
return doTransform(bytecode);
}
}
Rendimiento y Optimización¶
1. Minimizar Transformaciones¶
Solo transforma lo necesario.
// ✅ BIEN - Filtrado eficiente
public class OptimizedTransformer implements ClassTransformer {
private static final Set<String> TARGET_CLASSES = Set.of(
"com.hypixel.hytale.server.core.HytaleServer",
"com.hypixel.hytale.server.core.entity.entities.Player"
);
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// Retornar temprano si no es una clase objetivo
if (!TARGET_CLASSES.contains(className)) {
return null;
}
// Transformar solo clases específicas
return doTransform(className, bytecode);
}
}
2. Cachear Resultados¶
Cachea operaciones costosas.
public class CachingTransformer implements ClassTransformer {
private final Map<String, Pattern> patternCache = new ConcurrentHashMap<>();
private final Map<String, Boolean> classCheckCache = new ConcurrentHashMap<>();
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// Cachear verificación de clase
Boolean shouldTransform = classCheckCache.computeIfAbsent(
className,
this::shouldTransformClass
);
if (!shouldTransform) {
return null;
}
return doTransform(bytecode);
}
private boolean shouldTransformClass(String className) {
// Operación costosa, cachear resultado
Pattern pattern = patternCache.computeIfAbsent(
"server-classes",
k -> Pattern.compile("com\\.hypixel\\.hytale\\.server\\..*")
);
return pattern.matcher(className).matches();
}
}
3. Usar COMPUTE_FRAMES Inteligentemente¶
// ✅ BIEN - Usar COMPUTE_FRAMES solo cuando sea necesario
public class SmartFramesTransformer implements ClassTransformer {
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
ClassReader reader = new ClassReader(bytecode);
// Si solo modificamos código existente sin cambiar flujo de control,
// COMPUTE_MAXS es suficiente y más rápido
int flags = needsFrameComputation(className)
? ClassWriter.COMPUTE_FRAMES
: ClassWriter.COMPUTE_MAXS;
ClassWriter writer = new ClassWriter(reader, flags);
ClassVisitor visitor = new MyVisitor(Opcodes.ASM9, writer);
reader.accept(visitor, 0);
return writer.toByteArray();
}
private boolean needsFrameComputation(String className) {
// Solo necesitamos COMPUTE_FRAMES si modificamos el flujo de control
// (agregamos try-catch, loops, condicionales, etc.)
return className.equals("com.hypixel.hytale.server.core.ComplexClass");
}
}
4. Evitar Operaciones en el Hot Path¶
// ❌ MAL - Operaciones costosas en cada llamada
public class SlowTransformer extends MethodVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
// Compilar regex en cada invocación (MUY LENTO)
if (owner.matches("com\\.hypixel\\.hytale\\..*")) {
// ...
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
// ✅ BIEN - Precalcular y cachear
public class FastTransformer extends MethodVisitor {
private static final Pattern HYTALE_PATTERN =
Pattern.compile("com\\.hypixel\\.hytale\\..*");
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
if (HYTALE_PATTERN.matcher(owner).matches()) {
// ...
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
Seguridad y Validación¶
1. Validar Entrada del Usuario¶
public class SecureConfigManager {
public void loadConfig(Path configPath) {
// Validar path
if (!isSecurePath(configPath)) {
throw new SecurityException("Invalid config path: " + configPath);
}
// Validar que existe
if (!Files.exists(configPath)) {
createDefaultConfig(configPath);
return;
}
// Validar permisos
if (!Files.isReadable(configPath)) {
throw new SecurityException("Cannot read config file: " + configPath);
}
// Cargar config de forma segura
try (InputStream in = Files.newInputStream(configPath)) {
Properties props = new Properties();
props.load(in);
validateProperties(props);
applyConfig(props);
} catch (IOException e) {
throw new RuntimeException("Error loading config", e);
}
}
private boolean isSecurePath(Path path) {
// Verificar que no use path traversal
String normalized = path.normalize().toString();
return !normalized.contains("..") &&
normalized.startsWith("config/");
}
private void validateProperties(Properties props) {
// Validar valores
String maxPlayers = props.getProperty("max-players");
if (maxPlayers != null) {
try {
int value = Integer.parseInt(maxPlayers);
if (value < 1 || value > 1000) {
throw new IllegalArgumentException(
"max-players must be between 1 and 1000"
);
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"max-players must be a number"
);
}
}
}
}
2. Sandboxing de Transformaciones¶
public class SandboxedTransformer implements ClassTransformer {
private static final Set<String> FORBIDDEN_PACKAGES = Set.of(
"java.lang.System",
"java.lang.Runtime",
"java.lang.Process",
"java.io.File",
"java.nio.file.Files"
);
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// No transformar clases del sistema
if (className.startsWith("java.") ||
className.startsWith("javax.") ||
className.startsWith("sun.")) {
return null;
}
return doTransform(bytecode);
}
/**
* Visitor que previene acceso a clases peligrosas.
*/
private static class SecurityCheckVisitor extends MethodVisitor {
public SecurityCheckVisitor(int api, MethodVisitor mv) {
super(api, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
// Bloquear llamadas a métodos peligrosos
String ownerClass = owner.replace('/', '.');
if (FORBIDDEN_PACKAGES.contains(ownerClass)) {
throw new SecurityException(
"Attempted to call forbidden method: " +
ownerClass + "." + name
);
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
Compatibilidad Entre Versiones¶
1. Versionado Semántico¶
public class VersionedPlugin implements ClassTransformer {
public static final String VERSION = "2.1.0";
public static final int MAJOR_VERSION = 2;
public static final int MINOR_VERSION = 1;
public static final int PATCH_VERSION = 0;
// API compatible con versión mínima
private static final String MIN_HYTALE_VERSION = "1.0.0";
@Override
public int priority() {
return 50;
}
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// Verificar compatibilidad
if (!isCompatible()) {
System.err.println(
"[Plugin] Incompatible Hytale version. " +
"Required: " + MIN_HYTALE_VERSION
);
return null;
}
return doTransform(className, bytecode);
}
private boolean isCompatible() {
// Verificar versión de Hytale
String hytaleVersion = getHytaleVersion();
return compareVersions(hytaleVersion, MIN_HYTALE_VERSION) >= 0;
}
private String getHytaleVersion() {
// Obtener versión del servidor
// Implementación dependiente de cómo Hytale expone su versión
return "1.0.0";
}
private int compareVersions(String v1, String v2) {
String[] parts1 = v1.split("\\.");
String[] parts2 = v2.split("\\.");
for (int i = 0; i < Math.min(parts1.length, parts2.length); i++) {
int num1 = Integer.parseInt(parts1[i]);
int num2 = Integer.parseInt(parts2[i]);
if (num1 != num2) {
return Integer.compare(num1, num2);
}
}
return Integer.compare(parts1.length, parts2.length);
}
}
2. Feature Toggles¶
public class FeatureToggledPlugin implements ClassTransformer {
private final ConfigManager config;
public FeatureToggledPlugin() {
this.config = new ConfigManager("config/plugin.properties");
}
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
// Verificar features habilitadas
if (!config.getBoolean("features.transformation.enabled", true)) {
return null;
}
if (className.equals("com.hypixel.hytale.server.core.HytaleServer")) {
if (config.getBoolean("features.server-hooks.enabled", true)) {
return transformServer(bytecode);
}
}
if (className.equals("com.hypixel.hytale.server.core.entity.entities.Player")) {
if (config.getBoolean("features.player-hooks.enabled", true)) {
return transformPlayer(bytecode);
}
}
return null;
}
}
Testing y Debugging¶
1. Unit Testing¶
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class TransformerTest {
private MyTransformer transformer;
@BeforeEach
public void setUp() {
transformer = new MyTransformer();
}
@Test
public void testTransformTargetClass() {
byte[] original = loadClassBytes("com.example.TargetClass");
byte[] transformed = transformer.transform(
"com.example.TargetClass",
"com/example/TargetClass.class",
original
);
assertNotNull(transformed, "Should transform target class");
assertNotEquals(original, transformed, "Should modify bytecode");
// Verificar que el bytecode es válido
assertTrue(isValidBytecode(transformed), "Bytecode should be valid");
}
@Test
public void testIgnoreNonTargetClass() {
byte[] original = loadClassBytes("com.example.OtherClass");
byte[] result = transformer.transform(
"com.example.OtherClass",
"com/example/OtherClass.class",
original
);
assertNull(result, "Should not transform non-target class");
}
@Test
public void testHandleInvalidBytecode() {
byte[] invalid = new byte[] { 0x00, 0x01, 0x02 };
assertThrows(Exception.class, () -> {
transformer.transform("Invalid", "Invalid.class", invalid);
});
}
private byte[] loadClassBytes(String className) {
// Cargar bytes de clase de prueba
try (InputStream in = getClass().getResourceAsStream(
"/" + className.replace('.', '/') + ".class")) {
return in.readAllBytes();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean isValidBytecode(byte[] bytecode) {
try {
ClassReader reader = new ClassReader(bytecode);
ClassWriter writer = new ClassWriter(0);
CheckClassAdapter checker = new CheckClassAdapter(writer, true);
reader.accept(checker, 0);
return true;
} catch (Exception e) {
return false;
}
}
}
2. Logging Estructurado¶
public class StructuredLoggingTransformer implements ClassTransformer {
private static final Logger LOGGER = LoggerFactory.getLogger(
StructuredLoggingTransformer.class
);
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
long startTime = System.nanoTime();
try {
if (!shouldTransform(className)) {
LOGGER.debug("Skipping class: {}", className);
return null;
}
LOGGER.info("Transforming class: {}", className);
byte[] result = doTransform(bytecode);
long duration = System.nanoTime() - startTime;
LOGGER.info("Transformed {} in {}ms",
className,
duration / 1_000_000.0
);
return result;
} catch (Exception e) {
LOGGER.error("Error transforming class: {}", className, e);
return null;
}
}
}
3. Debug Mode¶
public class DebuggableTransformer implements ClassTransformer {
private static final boolean DEBUG = Boolean.getBoolean("plugin.debug");
private static final Path DEBUG_OUTPUT = Paths.get("debug", "transformed");
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
try {
byte[] result = doTransform(bytecode);
if (DEBUG && result != null) {
saveDebugInfo(className, bytecode, result);
}
return result;
} catch (Exception e) {
if (DEBUG) {
e.printStackTrace();
}
return null;
}
}
private void saveDebugInfo(String className, byte[] original, byte[] transformed) {
try {
// Guardar bytecode original
Path originalPath = DEBUG_OUTPUT.resolve("original")
.resolve(className.replace('.', '/') + ".class");
Files.createDirectories(originalPath.getParent());
Files.write(originalPath, original);
// Guardar bytecode transformado
Path transformedPath = DEBUG_OUTPUT.resolve("transformed")
.resolve(className.replace('.', '/') + ".class");
Files.createDirectories(transformedPath.getParent());
Files.write(transformedPath, transformed);
// Guardar comparación textual
Path diffPath = DEBUG_OUTPUT.resolve("diff")
.resolve(className.replace('.', '/') + ".txt");
Files.createDirectories(diffPath.getParent());
String diff = generateDiff(original, transformed);
Files.writeString(diffPath, diff);
System.out.println("[Debug] Saved debug info for " + className);
} catch (IOException e) {
System.err.println("[Debug] Error saving debug info: " + e.getMessage());
}
}
private String generateDiff(byte[] original, byte[] transformed) {
StringBuilder sb = new StringBuilder();
sb.append("=== ORIGINAL ===\n");
sb.append(disassemble(original));
sb.append("\n=== TRANSFORMED ===\n");
sb.append(disassemble(transformed));
return sb.toString();
}
private String disassemble(byte[] bytecode) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ClassReader reader = new ClassReader(bytecode);
TraceClassVisitor tracer = new TraceClassVisitor(pw);
reader.accept(tracer, 0);
return sw.toString();
}
}
Documentación y Mantenimiento¶
1. Documentar Transformaciones¶
/**
* Transforma la clase Player para agregar sistema de estadísticas personalizadas.
*
* <h3>Modificaciones:</h3>
* <ul>
* <li>Agrega campo: {@code private CustomStats customStats}</li>
* <li>Agrega método: {@code public CustomStats getCustomStats()}</li>
* <li>Modifica {@code takeDamage()} para actualizar estadísticas</li>
* </ul>
*
* <h3>Compatibilidad:</h3>
* <ul>
* <li>Versión mínima de Hytale: 1.0.0</li>
* <li>Requiere: CustomStatsPlugin v2.0+</li>
* </ul>
*
* <h3>Configuración:</h3>
* <pre>
* # config/player-stats.properties
* enabled=true
* track-damage=true
* track-deaths=true
* </pre>
*
* @author Tu Nombre
* @version 2.0.0
* @since 1.0.0
*/
public class PlayerStatsTransformer implements ClassTransformer {
// ...
}
2. Changelog¶
Mantén un registro de cambios.
/**
* Player Statistics Transformer
*
* @version 2.1.0
*
* <h3>Changelog:</h3>
* <ul>
* <li><b>2.1.0</b> (2026-01-15)
* <ul>
* <li>Added: Support for async stat updates</li>
* <li>Fixed: Race condition in stat calculation</li>
* <li>Changed: Improved performance by 30%</li>
* </ul>
* </li>
* <li><b>2.0.0</b> (2026-01-01)
* <ul>
* <li>Breaking: Changed stats API</li>
* <li>Added: Death tracking</li>
* <li>Removed: Deprecated methods</li>
* </ul>
* </li>
* <li><b>1.0.0</b> (2025-12-15)
* <ul>
* <li>Initial release</li>
* </ul>
* </li>
* </ul>
*/
public class PlayerStatsTransformer implements ClassTransformer {
// ...
}
3. README y Ejemplos¶
Proporciona documentación clara y ejemplos de uso.
# Player Stats Plugin
## Instalación
1. Descarga `player-stats-2.1.0.jar`
2. Coloca en `earlyplugins/`
3. Inicia servidor con `--accept-early-plugins`
## Configuración
```properties
# config/player-stats.properties
enabled=true
track-damage=true
track-deaths=true
save-interval=300
Uso¶
// Obtener stats de un jugador
Player player = ...;
CustomStats stats = player.getCustomStats();
System.out.println("Daño total: " + stats.getTotalDamage());
System.out.println("Muertes: " + stats.getDeaths());
API¶
## Patrones de Diseño Recomendados
### 1. Strategy Pattern
```java
public interface TransformationStrategy {
byte[] transform(byte[] bytecode);
}
public class LoggingStrategy implements TransformationStrategy {
@Override
public byte[] transform(byte[] bytecode) {
// Agregar logging
return bytecode;
}
}
public class ProfilingStrategy implements TransformationStrategy {
@Override
public byte[] transform(byte[] bytecode) {
// Agregar profiling
return bytecode;
}
}
public class StrategyBasedTransformer implements ClassTransformer {
private final Map<String, TransformationStrategy> strategies = new HashMap<>();
public StrategyBasedTransformer() {
strategies.put("logging", new LoggingStrategy());
strategies.put("profiling", new ProfilingStrategy());
}
@Override
public byte[] transform(String className, String classPath, byte[] bytecode) {
String strategy = getStrategyForClass(className);
if (strategy != null && strategies.containsKey(strategy)) {
return strategies.get(strategy).transform(bytecode);
}
return null;
}
}
2. Builder Pattern¶
public class TransformerBuilder {
private final List<String> targetClasses = new ArrayList<>();
private final List<MethodTransformation> transformations = new ArrayList<>();
private int priority = 50;
private boolean debug = false;
public TransformerBuilder targetClass(String className) {
this.targetClasses.add(className);
return this;
}
public TransformerBuilder addMethodTransformation(MethodTransformation transformation) {
this.transformations.add(transformation);
return this;
}
public TransformerBuilder withPriority(int priority) {
this.priority = priority;
return this;
}
public TransformerBuilder withDebug(boolean debug) {
this.debug = debug;
return this;
}
public ClassTransformer build() {
return new ConfiguredTransformer(
targetClasses,
transformations,
priority,
debug
);
}
}
// Uso:
ClassTransformer transformer = new TransformerBuilder()
.targetClass("com.hypixel.hytale.server.core.HytaleServer")
.addMethodTransformation(new LoggingTransformation())
.withPriority(100)
.withDebug(true)
.build();
Antipatrones a Evitar¶
❌ God Class¶
// MAL - Clase que hace demasiado
public class GodTransformer implements ClassTransformer {
// Transforma todo
// Maneja configuración
// Hace logging
// Gestiona estadísticas
// Maneja persistencia
// etc...
}
✅ Separación de Responsabilidades¶
// BIEN - Clases especializadas
public class ServerTransformer implements ClassTransformer { }
public class PlayerTransformer implements ClassTransformer { }
public class ConfigManager { }
public class StatisticsCollector { }
public class PersistenceManager { }
❌ Acoplamiento Fuerte¶
// MAL - Dependencias hardcodeadas
public class CoupledTransformer implements ClassTransformer {
private final SpecificLogger logger = new SpecificLogger();
private final SpecificConfig config = new SpecificConfig();
}
✅ Inyección de Dependencias¶
// BIEN - Dependencias inyectadas
public class DecoupledTransformer implements ClassTransformer {
private final Logger logger;
private final ConfigManager config;
public DecoupledTransformer(Logger logger, ConfigManager config) {
this.logger = logger;
this.config = config;
}
}
Checklist de Calidad¶
Antes de liberar tu plugin, verifica:
- Funcionalidad
- Todas las transformaciones funcionan correctamente
- Maneja casos edge correctamente
-
No rompe funcionalidad existente
-
Rendimiento
- Transformaciones son eficientes
- No hay memory leaks
-
Impacto en startup time es mínimo
-
Seguridad
- Valida todas las entradas
- No expone información sensible
-
No permite ejecución arbitraria de código
-
Compatibilidad
- Especifica versiones compatibles
- Maneja versiones antiguas gracefully
-
No rompe otros plugins
-
Testing
- Unit tests pasando
- Integration tests pasando
-
Probado en servidor real
-
Documentación
- README completo
- Javadoc en clases públicas
- Ejemplos de uso
-
Changelog actualizado
-
Código
- Sigue convenciones de Java
- No hay warnings del compilador
- Pasa linters (Checkstyle, SpotBugs)
Recursos Adicionales¶
- Creating Plugin: Guía básica de plugins
- Class Transformation: Técnicas avanzadas
- Bytecode Manipulation: Guía de ASM
- Effective Java: Libro recomendado
- Clean Code: Principios de código limpio
¿Problemas? Consulta Troubleshooting