Skip to content
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

implementation 'org.ow2.asm:asm:5.2'
implementation 'org.ow2.asm:asm-commons:5.2'
}

apply from: 'gradle/scripts/dependencies.gradle'
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ mixin_refmap = mixins.${mod_id}.refmap.json
# Only make a coremod if you are absolutely sure of what you are doing
# Change the property `coremod_includes_mod` to false if your coremod doesn't have a @Mod annotation
# You MUST state a class name for `coremod_plugin_class_name` if you are making a coremod, the class should implement `IFMLLoadingPlugin`
is_coremod = false
is_coremod = true
coremod_includes_mod = true
coremod_plugin_class_name =
coremod_plugin_class_name = world.maryt.rawinput.RawInputCorePlugin

# AssetMover
# Convenient way to allow downloading of assets from official vanilla Minecraft servers, CurseForge, or any direct links
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/world/maryt/rawinput/RawInputCorePlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package world.maryt.rawinput;

import java.util.Map;

import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;

/**
* Coremod plugin that transforms jinput classes with ASM
* This ensures our safety check is applied before problematic input devices can crash the game
*/
@IFMLLoadingPlugin.MCVersion("1.12.2")
@IFMLLoadingPlugin.Name("RawInput Coremod")
public class RawInputCorePlugin implements IFMLLoadingPlugin {
@Override
public String[] getASMTransformerClass() {
return new String[]{"world.maryt.rawinput.RawInputCoreTransformer"};
}

@Override
public String getModContainerClass() {
return null;
}

@Override
public String getSetupClass() {
return null;
}

@Override
public void injectData(Map<String, Object> data) {
// No additional data needed
}

@Override
public String getAccessTransformerClass() {
return null;
}
}
104 changes: 104 additions & 0 deletions src/main/java/world/maryt/rawinput/RawInputCoreTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package world.maryt.rawinput;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

/**
* ASM transformer that prevents crashes from problematic input devices
* by rejecting devices with num_buttons >= 32 before they can cause issues
*/
public class RawInputCoreTransformer implements net.minecraft.launchwrapper.IClassTransformer {

private static final Logger LOGGER = LogManager.getLogger("RawInputCoreTransformer");

@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
// Only transform jinput classes
if (!name.startsWith("net.java.games.input")) {
return basicClass;
}

// Target RawMouseInfo for the safety check
if (name.equals("net.java.games.input.RawMouseInfo")) {
LOGGER.info("Applying safety check to RawMouseInfo for problematic input devices");
return transformRawMouseInfo(basicClass);
}

return basicClass;
}

private byte[] transformRawMouseInfo(byte[] basicClass) {
try {
ClassReader reader = new ClassReader(basicClass);
ClassNode classNode = new ClassNode();
reader.accept(classNode, 0);

// Find the createControllerFromDevice method
for (Object methodObj : classNode.methods) {
MethodNode method = (MethodNode) methodObj;
if (method.name.equals("createControllerFromDevice") &&
method.desc.equals("(Lnet/java/games/input/RawDevice;Lnet/java/games/input/SetupAPIDevice;)Lnet/java/games/input/Controller;")) {

LOGGER.info("Injecting safety check: rejecting devices with num_buttons >= 32");

// Create the safety check: if (num_buttons >= 32) return null;
InsnList safetyCheck = new InsnList();

// Create a label for the jump target
LabelNode skipReturnLabel = new LabelNode();

// Load "this" (aload_0)
safetyCheck.add(new VarInsnNode(Opcodes.ALOAD, 0));

// Get the num_buttons field value
safetyCheck.add(new FieldInsnNode(
Opcodes.GETFIELD,
"net/java/games/input/RawMouseInfo",
"num_buttons",
"I"
));

// Load the constant 32
safetyCheck.add(new LdcInsnNode(32));

// Compare num_buttons >= 32 (if_icmplt means if num_buttons < 32, skip the return)
safetyCheck.add(new JumpInsnNode(Opcodes.IF_ICMPLT, skipReturnLabel));

// If num_buttons >= 32, return null
safetyCheck.add(new InsnNode(Opcodes.ACONST_NULL));
safetyCheck.add(new InsnNode(Opcodes.ARETURN));

// Add the label where execution should continue if num_buttons < 32
safetyCheck.add(skipReturnLabel);

// Insert the safety check at the start of the method
method.instructions.insertBefore(method.instructions.getFirst(), safetyCheck);

LOGGER.info("Safety check successfully injected");
break;
}
}

// Write the transformed class
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
classNode.accept(writer);
return writer.toByteArray();

} catch (Exception e) {
LOGGER.error("Failed to transform RawMouseInfo class", e);
return basicClass;
}
}
}