-
-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert Coremods from JS to Java (#785)
- Loading branch information
Showing
21 changed files
with
500 additions
and
263 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
plugins { | ||
id 'java-library' | ||
id 'com.diffplug.spotless' | ||
id 'net.neoforged.licenser' | ||
id 'neoforge.formatting-conventions' | ||
} | ||
|
||
repositories { | ||
maven { url = 'https://maven.neoforged.net/releases' } | ||
maven { | ||
name 'Mojang' | ||
url 'https://libraries.minecraft.net' | ||
} | ||
mavenCentral() | ||
} | ||
|
||
jar { | ||
manifest { | ||
attributes( | ||
"Automatic-Module-Name": "neoforge.coremods", | ||
FMLModType: "LIBRARY", | ||
) | ||
} | ||
} | ||
|
||
java { | ||
toolchain { | ||
languageVersion.set(JavaLanguageVersion.of(project.java_version)) | ||
} | ||
} | ||
|
||
dependencies { | ||
compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}" | ||
compileOnly "com.google.code.gson:gson:${gson_version}" | ||
compileOnly "org.slf4j:slf4j-api:${slf4j_api_version}" | ||
compileOnly "net.neoforged.fancymodloader:loader:${project.fancy_mod_loader_version}" | ||
} | ||
|
||
license { | ||
header = rootProject.file('codeformat/HEADER.txt') | ||
include '**/*.java' | ||
} | ||
|
||
tasks.withType(JavaCompile).configureEach { | ||
options.encoding = 'UTF-8' | ||
} |
79 changes: 79 additions & 0 deletions
79
coremods/src/main/java/net/neoforged/neoforge/coremods/CoremodUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright (c) NeoForged and contributors | ||
* SPDX-License-Identifier: LGPL-2.1-only | ||
*/ | ||
|
||
package net.neoforged.neoforge.coremods; | ||
|
||
import com.google.gson.Gson; | ||
import com.google.gson.reflect.TypeToken; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Objects; | ||
import org.jetbrains.annotations.Nullable; | ||
import org.objectweb.asm.tree.ClassNode; | ||
import org.objectweb.asm.tree.FieldNode; | ||
import org.objectweb.asm.tree.MethodNode; | ||
|
||
final class CoremodUtils { | ||
private static final Gson GSON = new Gson(); | ||
|
||
CoremodUtils() {} | ||
|
||
static <T> T loadResource(String filename, TypeToken<T> type) { | ||
var stream = NeoForgeCoreMod.class.getResourceAsStream(filename); | ||
if (stream == null) { | ||
throw new IllegalStateException("Missing resource: " + filename); | ||
} | ||
try (var reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { | ||
return GSON.fromJson(reader, type); | ||
} catch (IOException e) { | ||
throw new IllegalStateException("Failed to read JSON resource " + filename); | ||
} | ||
} | ||
|
||
static <T> T loadResource(String filename, Class<T> type) { | ||
return loadResource(filename, TypeToken.get(type)); | ||
} | ||
|
||
static FieldNode getFieldByName(ClassNode classNode, String fieldName) { | ||
FieldNode foundField = null; | ||
for (var fieldNode : classNode.fields) { | ||
if (Objects.equals(fieldNode.name, fieldName)) { | ||
if (foundField == null) { | ||
foundField = fieldNode; | ||
} else { | ||
throw new IllegalStateException("Found multiple fields with name " + fieldName + " in " + classNode.name); | ||
} | ||
} | ||
} | ||
if (foundField == null) { | ||
throw new IllegalStateException("No field with name " + fieldName + " found in class " + classNode.name); | ||
} | ||
return foundField; | ||
} | ||
|
||
static MethodNode getMethodByDescriptor(ClassNode classNode, @Nullable String methodName, String methodSignature) { | ||
MethodNode foundMethod = null; | ||
for (var methodNode : classNode.methods) { | ||
if (Objects.equals(methodNode.desc, methodSignature) | ||
&& (methodName == null || Objects.equals(methodNode.name, methodName))) { | ||
if (foundMethod == null) { | ||
foundMethod = methodNode; | ||
} else { | ||
throw new IllegalStateException("Found duplicate method with signature " + methodSignature + " in " + classNode.name); | ||
} | ||
} | ||
} | ||
|
||
if (foundMethod == null) { | ||
if (methodName != null) { | ||
throw new IllegalStateException("Unable to find method " + methodSignature + " with name " + methodName + " in " + classNode.name); | ||
} else { | ||
throw new IllegalStateException("Unable to find method " + methodSignature + " in " + classNode.name); | ||
} | ||
} | ||
return foundMethod; | ||
} | ||
} |
102 changes: 102 additions & 0 deletions
102
coremods/src/main/java/net/neoforged/neoforge/coremods/MethodRedirector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright (c) NeoForged and contributors | ||
* SPDX-License-Identifier: LGPL-2.1-only | ||
*/ | ||
|
||
package net.neoforged.neoforge.coremods; | ||
|
||
import cpw.mods.modlauncher.api.ITransformer; | ||
import cpw.mods.modlauncher.api.ITransformerVotingContext; | ||
import cpw.mods.modlauncher.api.TargetType; | ||
import cpw.mods.modlauncher.api.TransformerVoteResult; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import org.objectweb.asm.Opcodes; | ||
import org.objectweb.asm.tree.ClassNode; | ||
import org.objectweb.asm.tree.MethodInsnNode; | ||
|
||
/** | ||
* Redirect calls to one method to another. | ||
*/ | ||
public class MethodRedirector implements ITransformer<ClassNode> { | ||
private final Map<String, List<MethodRedirection>> redirectionsByClass = new HashMap<>(); | ||
private final Set<Target<ClassNode>> targets = new HashSet<>(); | ||
|
||
private static final List<MethodRedirection> REDIRECTIONS = List.of( | ||
new MethodRedirection( | ||
Opcodes.INVOKEVIRTUAL, | ||
"finalizeSpawn", | ||
"(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/MobSpawnType;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;", | ||
"finalize_spawn_targets.json", | ||
methodInsnNode -> new MethodInsnNode( | ||
Opcodes.INVOKESTATIC, | ||
"net/neoforged/neoforge/event/EventHooks", | ||
"finalizeMobSpawn", | ||
"(Lnet/minecraft/world/entity/Mob;Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/MobSpawnType;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;", | ||
false))); | ||
|
||
public MethodRedirector() { | ||
for (var redirection : REDIRECTIONS) { | ||
var targetClassNames = CoremodUtils.loadResource(redirection.targetClassListFile, String[].class); | ||
for (var targetClassName : targetClassNames) { | ||
targets.add(Target.targetClass(targetClassName)); | ||
var redirections = redirectionsByClass.computeIfAbsent(targetClassName, s -> new ArrayList<>()); | ||
redirections.add(redirection); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public TargetType<ClassNode> getTargetType() { | ||
return TargetType.CLASS; | ||
} | ||
|
||
@Override | ||
public Set<Target<ClassNode>> targets() { | ||
return targets; | ||
} | ||
|
||
@Override | ||
public ClassNode transform(ClassNode classNode, ITransformerVotingContext votingContext) { | ||
var redirections = redirectionsByClass.getOrDefault(classNode.name, Collections.emptyList()); | ||
|
||
var methods = classNode.methods; | ||
for (var method : methods) { | ||
var instr = method.instructions; | ||
for (var i = 0; i < instr.size(); i++) { | ||
var node = instr.get(i); | ||
if (node instanceof MethodInsnNode methodInsnNode) { | ||
for (var redirection : redirections) { | ||
if (redirection.invokeOpCode == methodInsnNode.getOpcode() | ||
&& redirection.methodName.equals(methodInsnNode.name) | ||
&& redirection.methodDescriptor.equals(methodInsnNode.desc)) { | ||
// Found a match for the target method | ||
instr.set( | ||
methodInsnNode, | ||
redirection.redirector.apply(methodInsnNode)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return classNode; | ||
} | ||
|
||
@Override | ||
public TransformerVoteResult castVote(ITransformerVotingContext context) { | ||
return TransformerVoteResult.YES; | ||
} | ||
|
||
private record MethodRedirection( | ||
int invokeOpCode, | ||
String methodName, | ||
String methodDescriptor, | ||
String targetClassListFile, | ||
Function<MethodInsnNode, MethodInsnNode> redirector) {} | ||
} |
30 changes: 30 additions & 0 deletions
30
coremods/src/main/java/net/neoforged/neoforge/coremods/NeoForgeCoreMod.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright (c) NeoForged and contributors | ||
* SPDX-License-Identifier: LGPL-2.1-only | ||
*/ | ||
|
||
package net.neoforged.neoforge.coremods; | ||
|
||
import cpw.mods.modlauncher.api.ITransformer; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import net.neoforged.neoforgespi.coremod.ICoreMod; | ||
|
||
public class NeoForgeCoreMod implements ICoreMod { | ||
@Override | ||
public Iterable<? extends ITransformer<?>> getTransformers() { | ||
List<ITransformer<?>> transformers = new ArrayList<>(); | ||
transformers.add(new ReplaceFieldWithGetterAccess("net.minecraft.world.level.biome.Biome", Map.of( | ||
"climateSettings", "getModifiedClimateSettings", | ||
"specialEffects", "getModifiedSpecialEffects"))); | ||
transformers.add(new ReplaceFieldWithGetterAccess("net.minecraft.world.level.levelgen.structure.Structure", Map.of( | ||
"settings", "getModifiedStructureSettings"))); | ||
transformers.add(new ReplaceFieldWithGetterAccess("net.minecraft.world.level.block.FlowerPotBlock", Map.of( | ||
"potted", "getPotted"))); | ||
|
||
transformers.add(new MethodRedirector()); | ||
|
||
return transformers; | ||
} | ||
} |
88 changes: 88 additions & 0 deletions
88
...s/src/main/java/net/neoforged/neoforge/coremods/ReplaceFieldComparisonWithInstanceOf.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* Copyright (c) NeoForged and contributors | ||
* SPDX-License-Identifier: LGPL-2.1-only | ||
*/ | ||
|
||
package net.neoforged.neoforge.coremods; | ||
|
||
import cpw.mods.modlauncher.api.ITransformer; | ||
import cpw.mods.modlauncher.api.ITransformerVotingContext; | ||
import cpw.mods.modlauncher.api.TargetType; | ||
import cpw.mods.modlauncher.api.TransformerVoteResult; | ||
import java.util.List; | ||
import java.util.Set; | ||
import org.objectweb.asm.Opcodes; | ||
import org.objectweb.asm.tree.FieldInsnNode; | ||
import org.objectweb.asm.tree.JumpInsnNode; | ||
import org.objectweb.asm.tree.MethodNode; | ||
import org.objectweb.asm.tree.TypeInsnNode; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* Replaces code such as {@code itemstack.getItem() == Items.CROSSBOW} with instanceof checks such | ||
* as {@code itemstack.getItem() instanceof CrossbowItem}. | ||
* This transformer targets a set of methods to replace the occurrence of a single field-comparison. | ||
*/ | ||
public class ReplaceFieldComparisonWithInstanceOf implements ITransformer<MethodNode> { | ||
private static final Logger LOG = LoggerFactory.getLogger(ReplaceFieldComparisonWithInstanceOf.class); | ||
|
||
private final Set<Target<MethodNode>> targets; | ||
private final String fieldOwner; | ||
private final String fieldName; | ||
private final String replacementClassName; | ||
|
||
/** | ||
* @param fieldOwner The class that owns {@code fieldName} | ||
* @param fieldName The name of a field in {@code fieldOwner} | ||
* @param replacementClassName Reference comparisons against {@code fieldName} in {@code fieldOwner} are replaced | ||
* by instanceof checks against this class. | ||
* @param methodsToScan The methods to scan | ||
*/ | ||
public ReplaceFieldComparisonWithInstanceOf(String fieldOwner, | ||
String fieldName, | ||
String replacementClassName, | ||
List<Target<MethodNode>> methodsToScan) { | ||
this.targets = Set.copyOf(methodsToScan); | ||
|
||
this.fieldOwner = fieldOwner; | ||
this.fieldName = fieldName; | ||
this.replacementClassName = replacementClassName; | ||
} | ||
|
||
@Override | ||
public TargetType<MethodNode> getTargetType() { | ||
return TargetType.METHOD; | ||
} | ||
|
||
@Override | ||
public Set<Target<MethodNode>> targets() { | ||
return targets; | ||
} | ||
|
||
@Override | ||
public MethodNode transform(MethodNode methodNode, ITransformerVotingContext votingContext) { | ||
var count = 0; | ||
for (var node = methodNode.instructions.getFirst(); node != null; node = node.getNext()) { | ||
if (node instanceof JumpInsnNode jumpNode && (jumpNode.getOpcode() == Opcodes.IF_ACMPEQ || jumpNode.getOpcode() == Opcodes.IF_ACMPNE)) { | ||
if (node.getPrevious() instanceof FieldInsnNode fieldAccessNode && (fieldAccessNode.getOpcode() == Opcodes.GETSTATIC || fieldAccessNode.getOpcode() == Opcodes.GETFIELD)) { | ||
if (fieldAccessNode.owner.equals(fieldOwner) && fieldAccessNode.name.equals(fieldName)) { | ||
methodNode.instructions.set(fieldAccessNode, new TypeInsnNode(Opcodes.INSTANCEOF, replacementClassName)); | ||
methodNode.instructions.set(jumpNode, new JumpInsnNode(jumpNode.getOpcode() == Opcodes.IF_ACMPEQ ? Opcodes.IFNE : Opcodes.IFEQ, jumpNode.label)); | ||
count++; | ||
} | ||
} | ||
} | ||
} | ||
|
||
LOG.trace("Transforming: {}.", methodNode.name); | ||
LOG.trace("field_to_instance: Replaced {} checks", count); | ||
|
||
return methodNode; | ||
} | ||
|
||
@Override | ||
public TransformerVoteResult castVote(ITransformerVotingContext context) { | ||
return TransformerVoteResult.YES; | ||
} | ||
} |
Oops, something went wrong.