summaryrefslogtreecommitdiff
path: root/src/main/java/ftbsc
diff options
context:
space:
mode:
author zaaarf <zaaarf@proton.me>2023-03-08 15:37:17 +0100
committer zaaarf <zaaarf@proton.me>2023-03-08 15:37:17 +0100
commitf6539d4a078e4cc37a56b9cdb548ba6a61a73b93 (patch)
tree47dfff577548d71e84e13b21120a1804626de8fe /src/main/java/ftbsc
parent51d9375e0bf42cc9342db9ee5f676988026b141b (diff)
feat: initial implementation of new system
- multiple injectors/target - removed strict limitation of one patch per class - made finders far smarter - added boolean for obfuscation environment (currently never changed from default)
Diffstat (limited to 'src/main/java/ftbsc')
-rw-r--r--src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java25
-rw-r--r--src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java17
-rw-r--r--src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java15
-rw-r--r--src/main/java/ftbsc/lll/processor/LilleroProcessor.java498
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/FindField.java2
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/FindMethod.java4
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/Injector.java7
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java11
-rw-r--r--src/main/java/ftbsc/lll/processor/annotations/Target.java13
-rw-r--r--src/main/java/ftbsc/lll/processor/tools/ASTUtils.java (renamed from src/main/java/ftbsc/lll/processor/ASTUtils.java)35
-rw-r--r--src/main/java/ftbsc/lll/processor/tools/SrgMapper.java211
11 files changed, 708 insertions, 130 deletions
diff --git a/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java b/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java
new file mode 100644
index 0000000..1befaa8
--- /dev/null
+++ b/src/main/java/ftbsc/lll/exceptions/AmbiguousDefinitionException.java
@@ -0,0 +1,25 @@
+package ftbsc.lll.exceptions;
+
+/**
+ * Thrown when the processor finds multiple methods matching the
+ * given criteria.
+ */
+public class AmbiguousDefinitionException extends RuntimeException {
+
+ /**
+ * Constructs a new ambiguous definition exception with the specified detail message.
+ * @param message the detail message
+ */
+ public AmbiguousDefinitionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new ambiguous definition exception with the specified detail message and cause.
+ * @param message the detail message
+ * @param cause the cause, may be null (indicating nonexistent or unknown cause)
+ */
+ public AmbiguousDefinitionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java b/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java
new file mode 100644
index 0000000..817761b
--- /dev/null
+++ b/src/main/java/ftbsc/lll/exceptions/MappingNotFoundException.java
@@ -0,0 +1,17 @@
+package ftbsc.lll.exceptions;
+
+import ftbsc.lll.processor.tools.SrgMapper;
+
+/**
+ * Thrown upon failure to find the requested mapping within a loaded {@link SrgMapper}.
+ */
+public class MappingNotFoundException extends RuntimeException {
+
+ /**
+ * Constructs a new mapping not found exception for the specified mapping.
+ * @param mapping the detail message
+ */
+ public MappingNotFoundException(String mapping) {
+ super("Could not find mapping for " + mapping + "!");
+ }
+}
diff --git a/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java
new file mode 100644
index 0000000..5be3d77
--- /dev/null
+++ b/src/main/java/ftbsc/lll/exceptions/TargetNotFoundException.java
@@ -0,0 +1,15 @@
+package ftbsc.lll.exceptions;
+
+/**
+ * Thrown upon failure to find an existing method from a stub.
+ */
+public class TargetNotFoundException extends RuntimeException {
+
+ /**
+ * Constructs a new target not found exception for the specified method stub.
+ * @param stub the stub's name (and descriptor possibly)
+ */
+ public TargetNotFoundException(String stub) {
+ super("Could not find member corresponding to stub: " + stub);
+ }
+}
diff --git a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java
index 501200a..0277211 100644
--- a/src/main/java/ftbsc/lll/processor/LilleroProcessor.java
+++ b/src/main/java/ftbsc/lll/processor/LilleroProcessor.java
@@ -2,33 +2,30 @@ package ftbsc.lll.processor;
import com.squareup.javapoet.*;
import ftbsc.lll.IInjector;
+import ftbsc.lll.exceptions.AmbiguousDefinitionException;
+import ftbsc.lll.exceptions.MappingNotFoundException;
+import ftbsc.lll.exceptions.TargetNotFoundException;
import ftbsc.lll.processor.annotations.*;
+import ftbsc.lll.processor.tools.SrgMapper;
import ftbsc.lll.proxies.FieldProxy;
import ftbsc.lll.proxies.MethodProxy;
-import ftbsc.lll.tools.SrgMapper;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.ExecutableType;
-import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.*;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
import java.util.stream.Collectors;
-import static ftbsc.lll.processor.ASTUtils.*;
+import static ftbsc.lll.processor.tools.ASTUtils.*;
/**
* The actual annotation processor behind the magic.
@@ -44,6 +41,11 @@ public class LilleroProcessor extends AbstractProcessor {
private final Set<String> generatedInjectors = new HashSet<>();
/**
+ * A static boolean that should be set to true when ran in a non-obfuscated environment.
+ */
+ public static boolean obfuscatedEnvironment = false; //todo: set this
+
+ /**
* Where the actual processing happens.
* It filters through whatever annotated class it's fed, and checks whether it contains
* the required information. It then generates injectors and a service provider for every
@@ -64,7 +66,7 @@ public class LilleroProcessor extends AbstractProcessor {
.filter(this::isValidInjector)
.collect(Collectors.toSet());
if(!validInjectors.isEmpty()) {
- validInjectors.forEach(this::generateInjector);
+ validInjectors.forEach(this::generateInjectors);
if (!this.generatedInjectors.isEmpty()) {
generateServiceProvider();
return true;
@@ -102,11 +104,190 @@ public class LilleroProcessor extends AbstractProcessor {
}
/**
- * Generates the Injector corresponding to the given class.
+ * Finds the class name and maps it to the correct format.
+ * @param patchAnn the {@link Patch} annotation containing target class info
+ * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null
+ * @param mapper the {@link SrgMapper} to use
+ * @implNote De facto, there is never any difference between the SRG and MCP name of a class.
+ * In theory, differences only arise between SRG/MCP names and Notch (fully obfuscated)
+ * names. However, this method still performs a conversion - just in case there is an
+ * odd one out.
+ * @return the fully qualified class name
+ * @since 0.3.0
+ */
+ private static String findClassName(Patch patchAnn, FindMethod methodAnn, SrgMapper mapper) {
+ String fullyQualifiedName =
+ methodAnn == null || methodAnn.parent() == Object.class
+ ? getClassFullyQualifiedName(patchAnn.value())
+ : getClassFullyQualifiedName(methodAnn.parent());
+ return mapper.mapClass(fullyQualifiedName, obfuscatedEnvironment).replace('/', '.');
+ }
+
+ /**
+ * Finds the class name and maps it to the correct format.
+ * @param patchAnn the {@link Patch} annotation containing target class info
+ * @param mapper the {@link SrgMapper} to use
+ * @return the internal class name
+ * @since 0.3.0
+ */
+ private static String findClassName(Patch patchAnn, SrgMapper mapper) {
+ return findClassName(patchAnn, null, mapper);
+ }
+
+ /**
+ * Finds the method name and maps it to the correct format.
+ * @param parentFQN the already mapped FQN of the parent class
+ * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null
+ * @param stub the {@link ExecutableElement} for the stub
+ * @param mapper the {@link SrgMapper} to use
+ * @return the internal class name
+ * @since 0.3.0
+ */
+ private static String findMethodName(String parentFQN, FindMethod methodAnn, ExecutableElement stub, SrgMapper mapper) {
+ String methodName = methodAnn == null ? stub.getSimpleName().toString() : methodAnn.name();
+ try {
+ methodName = mapper.mapMember(
+ parentFQN,
+ methodName,
+ obfuscatedEnvironment
+ );
+ } catch(MappingNotFoundException e) {
+ //not found: try again with the name of the annotated method
+ if(methodAnn == null) {
+ methodName = mapper.mapMember(
+ parentFQN,
+ stub.getSimpleName().toString(),
+ obfuscatedEnvironment
+ );
+ } else throw e;
+ }
+ return methodName;
+ }
+
+ /**
+ * Finds the method name and maps it to the correct format.
+ * @param patchAnn the {@link Patch} annotation containing target class info
+ * @param methodAnn the {@link FindMethod} annotation to fall back on, may be null
+ * @param stub the {@link ExecutableElement} for the stub
+ * @param mapper the {@link SrgMapper} to use
+ * @return the internal class name
+ * @since 0.3.0
+ */
+ private static String findMethodName(Patch patchAnn, FindMethod methodAnn, ExecutableElement stub, SrgMapper mapper) {
+ return findMethodName(findClassName(patchAnn, methodAnn, mapper), methodAnn, stub, mapper);
+ }
+
+ /**
+ * Finds a method given name, container and descriptor.
+ * @param fullyQualifiedNameParent the fully qualified name of the parent class of the method
+ * @param name the name to search for
+ * @param descr the descriptor to search for
+ * @param strict whether the search should be strict (see {@link Target#strict()} for more info)
+ * @return the desired method, if it exists
+ * @throws AmbiguousDefinitionException if it finds more than one candidate
+ * @throws TargetNotFoundException if it finds no valid candidate
+ * @since 0.3.0
+ */
+ private ExecutableElement findMethod(String fullyQualifiedNameParent, String name, String descr, boolean strict) {
+ TypeElement parent = processingEnv.getElementUtils().getTypeElement(fullyQualifiedNameParent);
+ if(parent == null)
+ throw new AmbiguousDefinitionException("Could not find parent class " + fullyQualifiedNameParent + "!");
+
+ //try to find by name
+ List<ExecutableElement> candidates = parent.getEnclosedElements()
+ .stream()
+ .filter(e -> e instanceof ExecutableElement)
+ .map(e -> (ExecutableElement) e)
+ .filter(e -> e.getSimpleName().contentEquals(name))
+ .collect(Collectors.toList());
+ if(candidates.size() == 0)
+ throw new TargetNotFoundException(name + " " + descr);
+ if(candidates.size() == 1 && !strict)
+ return candidates.get(0);
+ if(descr == null) {
+ throw new AmbiguousDefinitionException(
+ "Found " + candidates.size()
+ + " methods named " + name
+ + " in class " + fullyQualifiedNameParent + "!"
+ );
+ } else {
+ candidates = candidates.stream()
+ .filter(strict
+ ? c -> descr.equals(descriptorFromExecutableElement(c))
+ : c -> descr.split("\\)")[0].equalsIgnoreCase(descriptorFromExecutableElement(c).split("\\)")[0])
+ ).collect(Collectors.toList());
+ if(candidates.size() == 0)
+ throw new TargetNotFoundException(name + " " + descr);
+ if(candidates.size() > 1)
+ throw new AmbiguousDefinitionException(
+ "Found " + candidates.size()
+ + " methods named " + name
+ + " in class " + fullyQualifiedNameParent + "!"
+ );
+ return candidates.get(0);
+ }
+ }
+
+ /**
+ * Finds the real method corresponding to a stub.
+ * @param stub the {@link ExecutableElement} for the stub
+ * @param mapper the {@link SrgMapper} to use
+ * @return the desired method, if it exists
+ * @throws AmbiguousDefinitionException if it finds more than one candidate
+ * @throws TargetNotFoundException if it finds no valid candidate
+ * @since 0.3.0
+ */
+ private ExecutableElement findRealMethod(ExecutableElement stub, SrgMapper mapper) {
+ Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class);
+ FindMethod findAnn = stub.getAnnotation(FindMethod.class); //this may be null, it means no fallback info
+ Target target = stub.getAnnotation(Target.class); //if this is null strict mode is always disabled
+ String parentFQN = findClassName(patchAnn, findAnn, mapper);
+ String methodName = findMethodName(patchAnn, findAnn, stub, mapper);
+ return findMethod(
+ parentFQN,
+ methodName,
+ descriptorFromExecutableElement(stub),
+ target != null && target.strict());
+ }
+
+ /**
+ * Finds the real field corresponding to a stub.
+ * @param stub the {@link ExecutableElement} for the stub
+ * @param mapper the {@link SrgMapper} to use
+ * @return the desired method, if it exists
+ * @throws TargetNotFoundException if it finds no valid candidate
+ * @since 0.3.0
+ */
+ private VariableElement findField(ExecutableElement stub, SrgMapper mapper) {
+ Patch patchAnn = stub.getEnclosingElement().getAnnotation(Patch.class);
+ FindField fieldAnn = stub.getAnnotation(FindField.class);
+ String parentName = mapper.mapClass(getClassFullyQualifiedName(
+ fieldAnn.parent().equals(Object.class)
+ ? patchAnn.value()
+ : fieldAnn.parent()
+ ), obfuscatedEnvironment);
+ String name = fieldAnn.name().equals("")
+ ? stub.getSimpleName().toString()
+ : fieldAnn.name();
+ TypeElement parent = processingEnv.getElementUtils().getTypeElement(parentName);
+ List<VariableElement> candidates =
+ parent.getEnclosedElements()
+ .stream()
+ .filter(f -> f instanceof VariableElement)
+ .filter(f -> f.getSimpleName().contentEquals(name))
+ .map(f -> (VariableElement) f)
+ .collect(Collectors.toList());
+ if(candidates.size() == 0)
+ throw new TargetNotFoundException(stub.getSimpleName().toString());
+ else return candidates.get(0); //there can only ever be one
+ }
+
+ /**
+ * Generates the Injector(s) contained in the given class.
* Basically implements the {@link IInjector} interface for you.
* @param cl the {@link TypeElement} for the given class
*/
- private void generateInjector(TypeElement cl) {
+ private void generateInjectors(TypeElement cl) {
SrgMapper mapper;
try { //TODO: cant we get it from local?
URL url = new URL("https://data.fantabos.co/output.tsrg");
@@ -116,79 +297,149 @@ public class LilleroProcessor extends AbstractProcessor {
is.close();
} catch(IOException e) {
throw new RuntimeException("Could not open the specified TSRG file!", e);
- }
+ } //todo attempt to proceed without mappings
- Patch ann = cl.getAnnotation(Patch.class);
- String targetClassCanonicalName;
- try {
- targetClassCanonicalName = ann.value().getCanonicalName();
- } catch(MirroredTypeException e) {
- targetClassCanonicalName = e.getTypeMirror().toString();
- } //pretty sure class names de facto never change but better safe than sorry
- String targetClassSrgName = mapper.getMcpClass(targetClassCanonicalName.replace('.', '/'));
-
- ExecutableElement targetMethod = findAnnotatedMethods(cl, Target.class).get(0); //there should only be one
- String targetMethodDescriptor = descriptorFromMethodSpec(targetMethod);
- String targetMethodSrgName = mapper.getSrgMember(
- targetClassCanonicalName.replace('.', '/'),
- targetMethod.getSimpleName() + " " + targetMethodDescriptor
- );
-
- ExecutableElement injectorMethod = findAnnotatedMethods(cl, Injector.class).get(0); //there should only be one
+ //find class information
+ Patch patchAnn = cl.getAnnotation(Patch.class);
+ String targetClassSrgName = findClassName(patchAnn, mapper);
+ //find package information
Element packageElement = cl.getEnclosingElement();
while (packageElement.getKind() != ElementKind.PACKAGE)
packageElement = packageElement.getEnclosingElement();
-
String packageName = packageElement.toString();
- String injectorSimpleClassName = cl.getSimpleName().toString() + "Injector";
- String injectorClassName = packageName + "." + injectorSimpleClassName;
- MethodSpec stubOverride = MethodSpec.overriding(targetMethod)
- .addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called")
- .build();
+ //find injector(s) and target(s)
+ List<ExecutableElement> injectors = findAnnotatedMethods(cl, MultipleInjectors.class);
- MethodSpec inject = MethodSpec.methodBuilder("inject")
- .addModifiers(Modifier.PUBLIC)
- .returns(void.class)
- .addAnnotation(Override.class)
- .addParameter(ParameterSpec.builder(
- TypeName.get(processingEnv
- .getElementUtils()
- .getTypeElement("org.objectweb.asm.tree.ClassNode").asType()), "clazz").build())
- .addParameter(ParameterSpec.builder(
- TypeName.get(processingEnv
- .getElementUtils()
- .getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build())
- .addStatement("super." + injectorMethod.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType()))
- .build();
+ List<ExecutableElement> targets = findAnnotatedMethods(cl, Target.class);
- TypeSpec injectorClass = TypeSpec.classBuilder(injectorSimpleClassName)
- .addModifiers(Modifier.PUBLIC)
- .superclass(cl.asType())
- .addSuperinterface(ClassName.get(IInjector.class))
- .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString()))
- .addMethod(buildStringReturnMethod("reason", ann.reason()))
- .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.')))
- .addMethod(buildStringReturnMethod("methodName", targetMethodSrgName))
- .addMethod(buildStringReturnMethod("methodDesc", targetMethodDescriptor))
- .addMethods(generateRequestedProxies(cl, mapper))
- .addMethod(stubOverride)
- .addMethod(inject)
- .build();
+ //declare it once for efficiency
+ List<String> targetNames =
+ targets.stream()
+ .map(ExecutableElement::getSimpleName)
+ .map(Object::toString)
+ .collect(Collectors.toList());
- JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build();
+ //this will contain the classes to generate: the key is the class name
+ Map<String, InjectorInfo> toGenerate = new HashMap<>();
- try {
- JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(injectorClassName);
- PrintWriter out = new PrintWriter(injectorFile.openWriter());
- javaFile.writeTo(out);
- out.close();
- } catch(IOException e) {
- throw new RuntimeException(e);
+ for(ExecutableElement inj : injectors) {
+ MultipleInjectors minjAnn = inj.getAnnotation(MultipleInjectors.class);
+ int iterationNumber = 1;
+ for(Injector injectorAnn : minjAnn.value()) { //java is dumb
+ List<ExecutableElement> injectionCandidates = targets;
+
+ //case 1: it has a name, try to match it
+ if(!injectorAnn.targetName().equals("") && targetNames.contains(injectorAnn.targetName()))
+ injectionCandidates =
+ injectionCandidates
+ .stream()
+ .filter(i -> i.getSimpleName().toString().equals(injectorAnn.targetName()))
+ .collect(Collectors.toList());
+
+ //case 2: try to match by injectTargetName
+ String inferredName = inj.getSimpleName()
+ .toString()
+ .replaceFirst("inject", "");
+ injectionCandidates =
+ injectionCandidates
+ .stream()
+ .filter(t -> t.getSimpleName().toString().equalsIgnoreCase(inferredName))
+ .collect(Collectors.toList());
+
+ //case 3: there is only one target
+ if(targets.size() == 1)
+ injectionCandidates.add(targets.get(0));
+
+ ExecutableElement injectionTarget = null;
+
+ if(injectionCandidates.size() == 1)
+ injectionTarget = injectionCandidates.get(0);
+
+ if(injectorAnn.params().length != 0) {
+ StringBuilder descr = new StringBuilder("(");
+ for(Class<?> p : injectorAnn.params())
+ descr.append(descriptorFromType(TypeName.get(p)));
+ descr.append(")");
+ injectionCandidates =
+ injectionCandidates
+ .stream()
+ .filter(t -> //we care about arguments but not really about return type
+ descr.toString()
+ .split("\\)")[0]
+ .equalsIgnoreCase(descriptorFromExecutableElement(t).split("\\)")[0])
+ ).collect(Collectors.toList());
+ }
+
+ if(injectionCandidates.size() == 1)
+ injectionTarget = injectionCandidates.get(0);
+
+ //if we haven't found it yet, it's an ambiguity
+ if(injectionTarget == null)
+ throw new AmbiguousDefinitionException("Unclear target for injector " + inj.getSimpleName().toString() + "!");
+ else toGenerate.put(
+ cl.getSimpleName().toString() + "Injector" + iterationNumber,
+ new InjectorInfo(
+ inj, findRealMethod(
+ injectionTarget,
+ mapper
+ )
+ )
+ );
+ iterationNumber++;
+ }
}
- this.generatedInjectors.add(injectorClassName);
+ //iterate over the map and generate the classes
+ for(String injName : toGenerate.keySet()) {
+ MethodSpec stubOverride = MethodSpec.overriding(toGenerate.get(injName).target)
+ .addStatement("throw new $T($S)", RuntimeException.class, "This is a stub and should not have been called")
+ .build();
+
+ MethodSpec inject = MethodSpec.methodBuilder("inject")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(void.class)
+ .addAnnotation(Override.class)
+ .addParameter(ParameterSpec.builder(
+ TypeName.get(processingEnv
+ .getElementUtils()
+ .getTypeElement("org.objectweb.asm.tree.ClassNode").asType()), "clazz").build())
+ .addParameter(ParameterSpec.builder(
+ TypeName.get(processingEnv
+ .getElementUtils()
+ .getTypeElement("org.objectweb.asm.tree.MethodNode").asType()), "main").build())
+ .addStatement("super." + toGenerate.get(injName).injector.getSimpleName() + "(clazz, main)", TypeName.get(cl.asType()))
+ .build();
+
+ TypeSpec injectorClass = TypeSpec.classBuilder(injName)
+ .addModifiers(Modifier.PUBLIC)
+ .superclass(cl.asType())
+ .addSuperinterface(ClassName.get(IInjector.class))
+ .addMethod(buildStringReturnMethod("name", cl.getSimpleName().toString()))
+ .addMethod(buildStringReturnMethod("reason", patchAnn.reason()))
+ .addMethod(buildStringReturnMethod("targetClass", targetClassSrgName.replace('/', '.')))
+ .addMethod(buildStringReturnMethod("methodName", toGenerate.get(injName).target.getSimpleName().toString()))
+ .addMethod(buildStringReturnMethod("methodDesc", descriptorFromExecutableElement(toGenerate.get(injName).target)))
+ .addMethods(generateRequestedProxies(cl, mapper))
+ .addMethod(stubOverride)
+ .addMethod(inject)
+ .build();
+
+ JavaFile javaFile = JavaFile.builder(packageName, injectorClass).build();
+ String injectorClassName = packageName + "." + injName;
+
+ try {
+ JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(injectorClassName);
+ PrintWriter out = new PrintWriter(injectorFile.openWriter());
+ javaFile.writeTo(out);
+ out.close();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ this.generatedInjectors.add(injectorClassName);
+ }
}
/**
@@ -220,64 +471,44 @@ public class LilleroProcessor extends AbstractProcessor {
.filter(m -> !m.getModifiers().contains(Modifier.STATIC)) //skip static stuff as we can't override it
.filter(m -> !m.getModifiers().contains(Modifier.FINAL)) //in case someone is trying to be funny
.forEach(m -> {
- FindMethod ann = m.getAnnotation(FindMethod.class);
- String targetMethodName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name();
- try {
- MethodSpec.Builder b = MethodSpec.overriding(m);
- Method targetMethod = ann.parent().getMethod(
- targetMethodName,
- ann.params()
- );
- b.addStatement("$T bd = $T.builder($S)",
- MethodProxy.Builder.class,
- MethodProxy.class,
- targetMethodName
- );
- b.addStatement("bd.setParent($S)", targetMethod.getDeclaringClass().getCanonicalName());
- b.addStatement("bd.setModifier($L)", targetMethod.getModifiers());
- for(Class<?> p : targetMethod.getParameterTypes())
- b.addStatement("bd.addParameter($T.class)", p);
- b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType());
- b.addStatement("return bd.build()");
- generated.add(b.build());
- } catch(NoSuchMethodException e) {
- processingEnv.getMessager().printMessage(
- Diagnostic.Kind.ERROR,
- "Method not found: " + targetMethodName
- );
- }
+ ExecutableElement targetMethod = findRealMethod(m, mapper);
+ MethodSpec.Builder b = MethodSpec.overriding(m);
+ b.addStatement("$T bd = $T.builder($S)",
+ MethodProxy.Builder.class,
+ MethodProxy.class,
+ m.getSimpleName().toString()
+ );
+ b.addStatement("bd.setParent($S)", ((TypeElement) targetMethod.getEnclosingElement()).getQualifiedName().toString());
+ for(Modifier mod : targetMethod.getModifiers())
+ b.addStatement("bd.addModifier($L)", mapModifier(mod));
+ for(TypeParameterElement p : targetMethod.getTypeParameters())
+ b.addStatement("bd.addParameter($T.class)", p.asType());
+ b.addStatement("bd.setReturnType($T.class)", targetMethod.getReturnType());
+ b.addStatement("return bd.build()");
+ generated.add(b.build());
});
findAnnotatedMethods(cl, FindField.class)
.stream()
.filter(m -> !m.getModifiers().contains(Modifier.STATIC))
.filter(m -> !m.getModifiers().contains(Modifier.FINAL))
.forEach(m -> {
- FindField ann = m.getAnnotation(FindField.class);
- String targetFieldName = ann.name().equals("") ? m.getSimpleName().toString() : ann.name();
- try {
- MethodSpec.Builder b = MethodSpec.overriding(m);
- Field targetField = ann.parent().getField(targetFieldName);
- b.addStatement("$T bd = $T.builder($S)",
- FieldProxy.Builder.class,
- FieldProxy.class,
- targetFieldName
- );
- b.addStatement("bd.setParent($S)", targetField.getDeclaringClass().getCanonicalName());
- b.addStatement("bd.setModifier($L)", targetField.getModifiers());
- b.addStatement("bd.setType($T.class)", targetField.getType());
- b.addStatement("return bd.build()");
- generated.add(b.build());
- } catch(NoSuchFieldException e) {
- processingEnv.getMessager().printMessage(
- Diagnostic.Kind.ERROR,
- "Field not found: " + targetFieldName + " in class " + ann.parent().getCanonicalName()
- );
- }
+ VariableElement targetField = findField(m, mapper);
+ MethodSpec.Builder b = MethodSpec.overriding(m);
+ b.addStatement("$T bd = $T.builder($S)",
+ FieldProxy.Builder.class,
+ FieldProxy.class,
+ targetField.getSimpleName().toString()
+ );
+ b.addStatement("bd.setParent($S)", ((TypeElement) targetField.getEnclosingElement()).getQualifiedName().toString());
+ for(Modifier mod : targetField.getModifiers())
+ b.addStatement("bd.addModifier($L)", mapModifier(mod));
+ b.addStatement("bd.setType($T.class)", targetField.asType());
+ b.addStatement("return bd.build()");
+ generated.add(b.build());
});
return generated;
}
-
/**
* Generates the Service Provider file for the generated injectors.
*/
@@ -294,4 +525,31 @@ public class LilleroProcessor extends AbstractProcessor {
throw new RuntimeException(e);
}
}
+
+ /**
+ * Container for information about a class that is to be generated.
+ * Only used internally.
+ */
+ private static class InjectorInfo {
+ /**
+ * The {@link ExecutableElement} corresponding to the injector method.
+ */
+ public final ExecutableElement injector;
+
+
+ /**
+ * The {@link ExecutableElement} corresponding to the target method.
+ */
+ public final ExecutableElement target;
+
+ /**
+ * Public constructor.
+ * @param injector the injector {@link ExecutableElement}
+ * @param target the target {@link ExecutableElement}
+ */
+ public InjectorInfo(ExecutableElement injector, ExecutableElement target) {
+ this.injector = injector;
+ this.target = target;
+ }
+ }
} \ No newline at end of file
diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindField.java b/src/main/java/ftbsc/lll/processor/annotations/FindField.java
index 9b5a824..c13fd46 100644
--- a/src/main/java/ftbsc/lll/processor/annotations/FindField.java
+++ b/src/main/java/ftbsc/lll/processor/annotations/FindField.java
@@ -16,6 +16,6 @@ import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.CLASS)
@java.lang.annotation.Target(ElementType.METHOD)
public @interface FindField {
- Class<?> parent();
+ Class<?> parent() default Object.class;
String name() default "";
}
diff --git a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java
index bf93442..76fe560 100644
--- a/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java
+++ b/src/main/java/ftbsc/lll/processor/annotations/FindMethod.java
@@ -17,7 +17,7 @@ import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.CLASS)
@java.lang.annotation.Target(ElementType.METHOD)
public @interface FindMethod {
- Class<?> parent();
+ Class<?> parent() default Object.class;
String name() default "";
- Class<?>[] params();
+ Class<?>[] params() default {};
}
diff --git a/src/main/java/ftbsc/lll/processor/annotations/Injector.java b/src/main/java/ftbsc/lll/processor/annotations/Injector.java
index f5e22aa..c26f704 100644
--- a/src/main/java/ftbsc/lll/processor/annotations/Injector.java
+++ b/src/main/java/ftbsc/lll/processor/annotations/Injector.java
@@ -1,6 +1,7 @@
package ftbsc.lll.processor.annotations;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -14,5 +15,9 @@ import java.lang.annotation.RetentionPolicy;
* @see Target
*/
@Retention(RetentionPolicy.CLASS)
+@Repeatable(MultipleInjectors.class)
@java.lang.annotation.Target(ElementType.METHOD)
-public @interface Injector {}
+public @interface Injector {
+ String targetName() default "";
+ Class<?>[] params() default {};
+}
diff --git a/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java b/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java
new file mode 100644
index 0000000..5c6382e
--- /dev/null
+++ b/src/main/java/ftbsc/lll/processor/annotations/MultipleInjectors.java
@@ -0,0 +1,11 @@
+package ftbsc.lll.processor.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.CLASS)
+@java.lang.annotation.Target(ElementType.METHOD)
+public @interface MultipleInjectors {
+ Injector[] value();
+}
diff --git a/src/main/java/ftbsc/lll/processor/annotations/Target.java b/src/main/java/ftbsc/lll/processor/annotations/Target.java
index 885bd9c..38477d1 100644
--- a/src/main/java/ftbsc/lll/processor/annotations/Target.java
+++ b/src/main/java/ftbsc/lll/processor/annotations/Target.java
@@ -15,4 +15,15 @@ import java.lang.annotation.RetentionPolicy;
*/
@Retention(RetentionPolicy.CLASS)
@java.lang.annotation.Target(ElementType.METHOD)
-public @interface Target {}
+public @interface Target {
+
+ /**
+ * When set to false, tells the processor to first try to match a single method by name,
+ * and to only check parameters if further clarification is needed.
+ * @implNote While non-strict mode is more computationally efficient, it's ultimately not
+ * relevant, as it only matters at compile time. Do not set this to true unless
+ * you are sure know what you're doing.
+ * @since 0.3.0
+ */
+ boolean strict() default true;
+}
diff --git a/src/main/java/ftbsc/lll/processor/ASTUtils.java b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java
index eba5d89..c35e008 100644
--- a/src/main/java/ftbsc/lll/processor/ASTUtils.java
+++ b/src/main/java/ftbsc/lll/processor/tools/ASTUtils.java
@@ -1,4 +1,4 @@
-package ftbsc.lll.processor;
+package ftbsc.lll.processor.tools;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
@@ -9,6 +9,7 @@ import ftbsc.lll.tools.DescriptorBuilder;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import java.lang.annotation.Annotation;
import java.util.List;
@@ -35,12 +36,11 @@ public class ASTUtils {
}
/**
- * Builds a type descriptor from the given {@link TypeMirror}
- * @param t the {@link TypeMirror} representing the desired type
+ * Builds a type descriptor from the given {@link TypeName}.
+ * @param type the {@link TypeName} representing the desired type
* @return a {@link String} containing the relevant descriptor
*/
- public static String descriptorFromType(TypeMirror t) {
- TypeName type = TypeName.get(t);
+ public static String descriptorFromType(TypeName type) {
StringBuilder desc = new StringBuilder();
//add array brackets
while(type instanceof ArrayTypeName) {
@@ -74,6 +74,15 @@ public class ASTUtils {
}
/**
+ * Builds a type descriptor from the given {@link TypeMirror}.
+ * @param t the {@link TypeMirror} representing the desired type
+ * @return a {@link String} containing the relevant descriptor
+ */
+ public static String descriptorFromType(TypeMirror t) {
+ return descriptorFromType(TypeName.get(t));
+ }
+
+ /**
* Builds a method descriptor from the given {@link ExecutableElement}.
* @param m the {@link ExecutableElement} for the method
* @return a {@link String} containing the relevant descriptor
@@ -123,4 +132,20 @@ public class ASTUtils {
return 0;
}
}
+
+ /**
+ * Safely converts a {@link Class} to its fully qualified name. See
+ * <a href="https://area-51.blog/2009/02/13/getting-class-values-from-annotations-in-an-annotationprocessor">this blogpost</a>
+ * for more information.
+ * @param clazz the class to get the name for
+ * @return the fully qualified name of the given class
+ * @since 0.3.0
+ */
+ public static String getClassFullyQualifiedName(Class<?> clazz) {
+ try {
+ return clazz.getCanonicalName();
+ } catch(MirroredTypeException e) {
+ return e.getTypeMirror().toString();
+ }
+ }
}
diff --git a/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java b/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java
new file mode 100644
index 0000000..d3c9f79
--- /dev/null
+++ b/src/main/java/ftbsc/lll/processor/tools/SrgMapper.java
@@ -0,0 +1,211 @@
+package ftbsc.lll.processor.tools;
+
+import ftbsc.lll.exceptions.MappingNotFoundException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+/**
+ * Parses a .tsrg file into a mapper capable of converting from
+ * deobfuscated names to SRG names.
+ * Obviously, it may only be used at runtime if the .tsrg file is
+ * included in the resources. However, in that case, I'd recommend
+ * using the built-in Forge one and refrain from including an extra
+ * resource for no good reason.
+ * @since 0.2.0
+ */
+public class SrgMapper {
+
+ /**
+ * A Map using the deobfuscated names as keys,
+ * holding information for that Srg class as value.
+ */
+ private final Map<String, ObfuscationData> mapper = new HashMap<>();
+
+ /**
+ * The public constructor.
+ * Should be passed a {@link Stream} of Strings, one representing each line.
+ * Whether they contain line endings or not is irrelevant.
+ * @param str a {@link Stream} of strings
+ */
+ public SrgMapper(Stream<String> str) {
+ AtomicReference<String> currentClass = new AtomicReference<>("");
+ str.forEach(l -> {
+ if(l.startsWith("\t"))
+ mapper.get(currentClass.get()).addMember(l);
+ else {
+ ObfuscationData s = new ObfuscationData(l);
+ currentClass.set(s.mcpName);
+ mapper.put(s.mcpName, s);
+ }
+ });
+ }
+
+ /**
+ * Gets the SRG-obfuscated name of the class.
+ * @param mcp the MCP (deobfuscated) internal name of the desired class
+ * @return the SRG name of the class
+ * @throws MappingNotFoundException if no mapping is found
+ */
+ public String getSrgClass(String mcp) {
+ ObfuscationData data = mapper.get(mcp);
+ if(data == null)
+ throw new MappingNotFoundException(mcp);
+ else return data.srgName;
+ }
+
+ /**
+ * Gets the MCP (deobfuscated) name of the class.
+ * Due to how it's implemented, it's considerably less efficient than its
+ * opposite operation.
+ * @param srg the SRG-obfuscated internal name of the desired class
+ * @return the MCP name of the class
+ */
+ public String getMcpClass(String srg) {
+ ObfuscationData data = getObfuscationData(srg);
+ return data.mcpName;
+ }
+
+ /**
+ * Gets one between the SRG and MCP names.
+ * @param name the internal name of the desired class in either format
+ * @param obf whether it should return the obfuscated name
+ * @return a {@link String} containing the internal name of the class
+ * @throws MappingNotFoundException if no mapping is found
+ * @since 0.3.0
+ */
+ public String mapClass(String name, boolean obf) {
+ String srg;
+ try {
+ srg = this.getSrgClass(name);
+ } catch(MappingNotFoundException e) {
+ srg = name;
+ name = this.getMcpClass(srg);
+ }
+ if(obf) return srg;
+ else return name;
+ }
+
+ /**
+ * Gets the SRG-obfuscated name of a class member (field or method).
+ * The method signature must be in this format: "methodName methodDescriptor",
+ * with a space, because that's how it is in .tsrg files.
+ * @param mcpClass the MCP (deobfuscated) internal name of the container class
+ * @param member the field name or method signature
+ * @return the SRG name of the given member
+ * @throws MappingNotFoundException if no mapping is found
+ */
+ public String getSrgMember(String mcpClass, String member) {
+ ObfuscationData data = mapper.get(mcpClass);
+ if(data == null)
+ throw new MappingNotFoundException(mcpClass + "::" + member);
+ return data.members.get(member);
+ }
+
+ /**
+ * Gets the MCP (deobfuscated) name of the given member.
+ * Due to how it's implemented, it's considerably less efficient than its
+ * opposite operation.
+ * @param srgClass the SRG-obfuscated internal name of the container class
+ * @param member the field name or method signature
+ * @return the MCP name of the given member
+ */
+ public String getMcpMember(String srgClass, String member) {
+ ObfuscationData data = getObfuscationData(srgClass);
+ for(String mcp : data.members.keySet())
+ if(data.members.get(mcp).equals(member))
+ return mcp;
+ return null;
+ }
+
+ /**
+ * Obfuscates or deobfuscates a member, given one of its names and the effective.
+ * @param className the internal or fully qualified name of the container class
+ * @param memberName the member of the class
+ * @param obf whether it should return the obfuscated name
+ * @return the mapped member name
+ * @throws MappingNotFoundException if no mapping is found
+ * @since 0.3.0
+ */
+ public String mapMember(String className, String memberName, boolean obf) {
+ className = className.replace('.', '/');
+ String effectiveClassName = this.mapClass(className, obf);
+ String srgMemberName;
+ try {
+ srgMemberName = this.getSrgMember(effectiveClassName, memberName);
+ } catch(MappingNotFoundException e) {
+ srgMemberName = memberName;
+ memberName = this.getMcpMember(effectiveClassName, memberName);
+ }
+ if(obf) return srgMemberName;
+ else return memberName;
+ }
+
+ /**
+ * Used internally. Gets the obfuscation data corresponding to the given SRG name.
+ * @return the desired {@link ObfuscationData} object
+ * @throws MappingNotFoundException if no {@link ObfuscationData} object is found
+ */
+ private ObfuscationData getObfuscationData(String srg) {
+ for(ObfuscationData s : mapper.values())
+ if(s.srgName.equals(srg))
+ return s;
+ throw new MappingNotFoundException(srg);
+ }
+
+ /**
+ * Private class used internally for storing information about each
+ * class. It's private because there is no good reason anyone would
+ * want to access this outside of this class.
+ */
+ private static class ObfuscationData {
+ /**
+ * The MCP internal name (FQN with '/' instad of '.') of the class.
+ */
+ private final String mcpName;
+
+ /**
+ * The SRG internal name (FQN with '/' instad of '.') of the class.
+ */
+ private final String srgName;
+
+ /**
+ * A {@link Map} tying each member's deobfuscated name or signature to its
+ * SRG name.
+ */
+ private final Map<String, String> members;
+
+
+ /**
+ * The constructor. It takes in the line where the class is declared,
+ * which looks something like this:
+ * {@code internal/name/mcp internal/name/srg }
+ * @param s the String represeting the declaration line
+ */
+ private ObfuscationData(String s) {
+ String[] split = s.trim().split(" ");
+ this.mcpName = split[0];
+ this.srgName = split[1];
+ this.members = new HashMap<>();
+ }
+
+ /**
+ * Adds a member to the target class. It takes in the line where the
+ * member is declared.
+ * For fields it looks like this:
+ * {@code fieldMcpName field_srg_name}
+ * For methods it looks like this:
+ * {@code methodName methodDescriptor method_srg_name}
+ * @param s the String representing the declaration line
+ */
+ public void addMember(String s) {
+ String[] split = s.trim().split(" ");
+ if(split.length == 2) //field
+ members.put(split[0], split[1]);
+ else if (split.length == 3) //method
+ members.put(split[0] + " " + split[1], split[2]);
+ }
+ }
+}