forked from OpenCubicChunks/CubicChunks3
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathASMConfigPlugin.java
More file actions
314 lines (277 loc) · 14.4 KB
/
ASMConfigPlugin.java
File metadata and controls
314 lines (277 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package io.github.opencubicchunks.cubicchunks.mixin;
import static org.objectweb.asm.Opcodes.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.mojang.datafixers.util.Either;
import io.github.notstirred.dasm.annotation.AnnotationParser;
import io.github.notstirred.dasm.api.annotations.transform.ApplicationStage;
import io.github.notstirred.dasm.api.provider.MappingsProvider;
import io.github.notstirred.dasm.exception.DasmException;
import io.github.notstirred.dasm.notify.Notification;
import io.github.notstirred.dasm.transformer.Transformer;
import io.github.notstirred.dasm.transformer.data.ClassTransform;
import io.github.notstirred.dasm.transformer.data.MethodTransform;
import io.github.notstirred.dasm.util.CachingClassProvider;
import io.github.notstirred.dasm.util.NotifyStack;
import io.github.notstirred.dasm.util.Pair;
import io.github.opencubicchunks.cc_core.annotation.Public;
import io.github.opencubicchunks.cubicchunks.CubicChunks;
import net.minecraft.Util;
import net.neoforged.fml.loading.FMLEnvironment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import org.spongepowered.asm.mixin.transformer.ClassInfo;
import org.spongepowered.asm.service.MixinService;
public class ASMConfigPlugin implements IMixinConfigPlugin {
private final Map<String, Boolean> dasmTransformedInPreApply = new ConcurrentHashMap<>();
private final Transformer transformer;
private final AnnotationParser annotationParser;
private final Map<String, Either<ClassTransform, Collection<MethodTransform>>> preApplyTargets = new HashMap<>();
private final Map<String, Either<ClassTransform, Collection<MethodTransform>>> postApplyTargets = new HashMap<>();
private final Logger logger = LogManager.getLogger("dasm");
@SuppressWarnings("checkstyle:EmptyCatchBlock") // <-- TODO stirred's problem not mine :)
public ASMConfigPlugin() {
boolean developmentEnvironment = false;
try {
developmentEnvironment = !FMLEnvironment.production;
} catch (Throwable ignored) {}
MappingsProvider mappings = MappingsProvider.IDENTITY;
// TODO: breaks on fabric (remapped at runtime)
var classProvider = new CachingClassProvider(s -> {
try (var classStream = ASMConfigPlugin.class.getClassLoader().getResourceAsStream(s.replace(".", "/") + ".class")) {
return Optional.ofNullable(classStream.readAllBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
this.transformer = new Transformer(classProvider, mappings);
this.annotationParser = new AnnotationParser(classProvider);
}
@Override public void onLoad(String mixinPackage) {}
@Override public String getRefMapperConfig() {
return null;
}
@Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
try {
ClassNode targetClass = MixinService.getService().getBytecodeProvider().getClassNode(targetClassName);
ClassNode mixinClass = MixinService.getService().getBytecodeProvider().getClassNode(mixinClassName);
mixinClass.name = targetClass.name;
// PRE_APPLY
handleError(this.annotationParser.findDasmAnnotations(mixinClass));
var methodTransformsMixin = handleError(this.annotationParser.buildContext().buildMethodTargets(mixinClass, "cc_dasm$"));
handleError(this.annotationParser.findDasmAnnotations(targetClass));
var classTransform = handleError(this.annotationParser.buildContext().buildClassTarget(targetClass));
var methodTransformsTarget = handleError(this.annotationParser.buildContext().buildMethodTargets(targetClass, "cc_dasm$"));
var methodTransforms = Stream.of(methodTransformsTarget, methodTransformsMixin).filter(Optional::isPresent).map(Optional::get)
.flatMap(Collection::stream).toList();
String key = mixinClassName + "|" + targetClassName;
if (classTransform.isPresent()) {
// TODO: nice error
assert methodTransformsMixin.isEmpty() && methodTransforms.isEmpty() : "Whole class transform WITH method transforms?";
ClassTransform transform = classTransform.get();
if (transform.stage() == ApplicationStage.PRE_APPLY) {
this.preApplyTargets.put(key, Either.left(transform));
} else {
this.postApplyTargets.put(key, Either.left(transform));
}
} else {
Collection<MethodTransform> preTransforms = this.preApplyTargets.computeIfAbsent(key, k -> Either.right(new ArrayList<>())).right()
.get();
Collection<MethodTransform> postTransforms = this.postApplyTargets.computeIfAbsent(key, k -> Either.right(new ArrayList<>())).right()
.get();
methodTransforms.forEach(transform -> {
if (transform.stage() == ApplicationStage.PRE_APPLY) {
preTransforms.add(transform);
} else {
postTransforms.add(transform);
}
});
}
} catch (DasmException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
@Override public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {}
@Override public @Nullable List<String> getMixins() {
return null;
}
@Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
boolean wasTransformed;
try {
wasTransformed = transformClass(targetClassName, targetClass, mixinClassName, ApplicationStage.PRE_APPLY);
} catch (DasmException e) {
throw new RuntimeException(e);
}
dasmTransformedInPreApply.put(mixinClassName + "|" + targetClassName, wasTransformed);
try {
// ugly hack to add class metadata to mixin
// based on
// https://github.com/Chocohead/OptiFabric/blob/54fc2ef7533e43d1982e14bc3302bcf156f590d8/src/main/java/me/modmuss50/optifabric/compat/fabricrendererapi
// /RendererMixinPlugin.java#L25:L44
Method addMethod = ClassInfo.class.getDeclaredMethod("addMethod", MethodNode.class, boolean.class);
addMethod.setAccessible(true);
ClassInfo ci = ClassInfo.forName(targetClassName);
Set<String> existingMethods = ci.getMethods().stream().map(x -> x.getName() + x.getDesc()).collect(Collectors.toSet());
for (MethodNode method : targetClass.methods) {
if (!existingMethods.contains(method.name + method.desc)) {
addMethod.invoke(ci, method, false);
}
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
@Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
doPublicAnnotation(targetClass);
// Apply POST_APPLY dasm transforms
boolean wasTransformed;
try {
wasTransformed = transformClass(targetClassName, targetClass, mixinClassName, ApplicationStage.POST_APPLY);
} catch (DasmException e) {
throw new RuntimeException(e);
}
// If no DASM transformation happened to this class, we can skip removing the prefixed methods
if (!(wasTransformed | dasmTransformedInPreApply.get(mixinClassName + "|" + targetClassName))) {
return;
}
// Find all DASM-added method nodes and their corresponding MixinMerged method nodes
record PrefixMethodPair(MethodNode dasmAddedMethod, MethodNode mixinAddedMethod) {}
List<PrefixMethodPair> methodPairs = new ArrayList<>();
for (MethodNode methodNode : targetClass.methods) {
if (methodNode.name.contains("cc_dasm$")) {
var methodNameWithoutPrefix = methodNode.name.substring(methodNode.name.indexOf("$") + 1).replace("__init__", "<init>")
.replace("__clinit__", "<clinit>");
var mixinAddedMethod = targetClass.methods.stream()
.filter(m -> m.name.equals(methodNameWithoutPrefix) && m.desc.equals(methodNode.desc)).findFirst();
if (mixinAddedMethod.isEmpty()) {
CubicChunks.LOGGER
.info(String.format("Found DASM added method `%s` without a corresponding MixinMerged method", methodNameWithoutPrefix));
}
methodPairs.add(new PrefixMethodPair(methodNode, mixinAddedMethod.orElse(null)));
}
}
// Remove the mixin-added methods and set the dasm-added names
methodPairs.forEach(prefixMethodPair -> {
if (prefixMethodPair.mixinAddedMethod != null) {
targetClass.methods.remove(prefixMethodPair.mixinAddedMethod);
// Copy annotations and visibility from mixin method
prefixMethodPair.dasmAddedMethod.visibleAnnotations = prefixMethodPair.mixinAddedMethod.visibleAnnotations;
prefixMethodPair.dasmAddedMethod.invisibleAnnotations = prefixMethodPair.mixinAddedMethod.invisibleAnnotations;
prefixMethodPair.dasmAddedMethod.access &= ~(ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED);
prefixMethodPair.dasmAddedMethod.access |= (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED) & prefixMethodPair.mixinAddedMethod.access;
}
prefixMethodPair.dasmAddedMethod.name = prefixMethodPair.dasmAddedMethod.name.replace("__init__", "<init>").replace("__clinit__",
"<clinit>");
// remove the prefix
prefixMethodPair.dasmAddedMethod.name = prefixMethodPair.dasmAddedMethod.name.substring("cc_dasm$".length());
});
ClassWriter classWriter = new ClassWriter(0);
targetClass.accept(classWriter);
try {
Path path = Path.of(".dasm.out/" + "POSTIER_APPLY" + "/" + targetClassName.replace('.', '/') + ".class").toAbsolutePath();
Files.createDirectories(path.getParent());
Files.write(path, classWriter.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void doPublicAnnotation(ClassNode targetClass) {
// Making any methods annotated as @Public public
for (MethodNode method : targetClass.methods) {
List<AnnotationNode> visibleAnnotations = method.visibleAnnotations;
if (visibleAnnotations != null) {
if (visibleAnnotations.stream()
.anyMatch(annotationNode -> annotationNode.desc.equals("L" + Public.class.getName().replace('.', '/') + ";"))) {
method.access &= ~(ACC_PRIVATE | ACC_PROTECTED);
method.access |= ACC_PUBLIC;
}
}
}
}
/**
* @return Whether any transformation was done to the targetClass
*/
private boolean transformClass(String targetClassName, ClassNode targetClass, String mixinClassName, ApplicationStage stage)
throws DasmException {
Either<ClassTransform, Collection<MethodTransform>> target = null;
switch (stage) {
case PRE_APPLY -> {
target = preApplyTargets.get(mixinClassName + "|" + targetClassName);
}
case POST_APPLY -> {
target = postApplyTargets.get(mixinClassName + "|" + targetClassName);
}
default -> throw new IllegalStateException("Unknown enum variant: " + stage);
}
if (target == null) {
return false;
}
if (target.left().isPresent()) {
this.transformer.transform(targetClass, target.left().get());
} else {
this.transformer.transform(targetClass, target.right().get());
}
ClassWriter classWriter = new ClassWriter(0);
targetClass.accept(classWriter);
try {
Path path = Path.of(".dasm.out/" + stage + "/" + targetClassName.replace('.', '/') + ".class").toAbsolutePath();
Files.createDirectories(path.getParent());
Files.write(path, classWriter.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
private <T> T handleError(Pair<T, List<Notification>> result) {
handleError(result.second());
return result.first();
}
private void handleError(NotifyStack notifyStack) {
handleError(notifyStack.notifications());
}
private void handleError(List<Notification> notifications) {
for (var notification : notifications) {
switch (notification.kind) {
case INFO:
logger.info(notification.message);
break;
case WARNING:
logger.warn(notification.message);
break;
case ERROR:
logger.error(notification.message);
break;
default:
throw new IllegalStateException("Unknown enum variant: " + notification.kind);
}
}
if (notifications.stream().anyMatch(n -> n.kind == Notification.Kind.ERROR)) {
throw Util.pauseInIde(new RuntimeException("DASM Failure, please see log output"));
}
}
}