Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Coremods from JS to Java #785

Open
wants to merge 8 commits into
base: 1.21.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
shartte marked this conversation as resolved.
Show resolved Hide resolved
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