Skip to content

Commit

Permalink
Convert Coremods from JS to Java (#785)
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte authored Sep 19, 2024
1 parent e65f2b5 commit 46112b3
Show file tree
Hide file tree
Showing 21 changed files with 500 additions and 263 deletions.
46 changes: 46 additions & 0 deletions coremods/build.gradle
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'
}
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;
}
}
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) {}
}
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;
}
}
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;
}
}
Loading

0 comments on commit 46112b3

Please sign in to comment.