diff --git a/examples/postInit/custom/experiments.groovy b/examples/postInit/custom/experiments.groovy new file mode 100644 index 000000000..114319624 --- /dev/null +++ b/examples/postInit/custom/experiments.groovy @@ -0,0 +1,16 @@ + +// no_run + +def ore_iron = ore('ingotIron') +def item_iron = item('minecraft:iron_ingot') +log.info(item_iron in ore_iron) // true +log.info(item_iron in item_iron) // true +log.info(ore_iron in item_iron) // false +log.info(item_iron << ore_iron) // true +log.info((item_iron * 3) << ore_iron) // false +log.info(ore_iron >> item_iron) // true +log.info(ore_iron >> (item_iron * 3)) // false + +file('config/').eachFile { file -> + println file.path +} diff --git a/examples/postInit/custom/reflection.groovy b/examples/postInit/custom/reflection.groovy new file mode 100644 index 000000000..38e02c6e5 --- /dev/null +++ b/examples/postInit/custom/reflection.groovy @@ -0,0 +1,17 @@ + +// side: client + +import net.minecraft.client.gui.GuiMainMenu +import net.minecraftforge.client.event.GuiOpenEvent + +// not a typo +GuiMainMenu.metaClass.makePublic('minceraftRoll') +GuiMainMenu.metaClass.makeMutable('minceraftRoll') + +eventManager.listen(GuiOpenEvent) { + if (gui instanceof GuiMainMenu) { + // value is randomly set in constructor and checked if its smaller than 1e-4 during rendering + // this forces the minceraft title to always activate + gui.minceraftRoll = 0.00001 + } +} diff --git a/examples/postInit/custom/vanilla.groovy b/examples/postInit/custom/vanilla.groovy index 4c3405ebc..6d442f2ab 100644 --- a/examples/postInit/custom/vanilla.groovy +++ b/examples/postInit/custom/vanilla.groovy @@ -5,22 +5,6 @@ import net.minecraftforge.event.entity.living.EnderTeleportEvent import net.minecraftforge.event.world.BlockEvent import net.minecraft.util.text.TextComponentString -/* -def ore_iron = ore('ingotIron') -def item_iron = item('minecraft:iron_ingot') -log.info(item_iron in ore_iron) // true -log.info(item_iron in item_iron) // true -log.info(ore_iron in item_iron) // false -log.info(item_iron << ore_iron) // true -log.info((item_iron * 3) << ore_iron) // false -log.info(ore_iron >> item_iron) // true -log.info(ore_iron >> (item_iron * 3)) // false -*/ - -/*file('config/').eachFile { file -> - println file.path -}*/ - for (var stack in mods.minecraft.allItems[5..12]) { log.info stack } diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/MetaClassExpansion.java b/src/main/java/com/cleanroommc/groovyscript/helper/MetaClassExpansion.java new file mode 100644 index 000000000..5f9ca13bf --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/helper/MetaClassExpansion.java @@ -0,0 +1,63 @@ +package com.cleanroommc.groovyscript.helper; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import groovy.lang.MetaClass; +import groovy.lang.MetaMethod; +import groovy.lang.MetaProperty; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.reflection.CachedField; +import org.codehaus.groovy.reflection.CachedMethod; + +public class MetaClassExpansion { + + /** + * Allows making any fields or methods with a specific name public. Logs an error if there is no method or field with that name. The member must be a normal + * field/method from java and not some special cases like {@link groovy.lang.MetaBeanProperty} or other injected members via groovy. + * Note that the {@link java.lang.reflect.Field Field} instance groovy stores and the one you get from {@link Class#getDeclaredField(String)} are different. + * This means that calling this method will make the member only public for groovy, but not for java. + * + * @param mc self meta class + * @param memberName name of members to make public + */ + public static void makePublic(MetaClass mc, String memberName) { + boolean success = false; + MetaProperty mp = mc.getMetaProperty(memberName); + if (mp instanceof CachedField cachedField) { + ReflectionHelper.makeFieldPublic(cachedField.getCachedField()); + success = true; + } + for (MetaMethod mm : mc.getMethods()) { + if (memberName.equals(mm.getName()) && mm instanceof CachedMethod cachedMethod) { + ReflectionHelper.makeMethodPublic(cachedMethod.getCachedMethod()); + success = true; + } + } + if (!success) { + GroovyLog.get().error("Failed to make member '{}' of class {} public, because no member was found!", memberName, getName(mc)); + } + } + + /** + * Allows making a field with a specific name non-final. Does nothing if the field is already non-final. + * Logs an error if there is no field with that name. The field must be a normal field from java and not some special cases like + * {@link groovy.lang.MetaBeanProperty} or other injected members via groovy. Note that the {@link java.lang.reflect.Field Field} instance groovy stores and + * the one you get from {@link Class#getDeclaredField(String)} are different. This means that calling this method will make the member only non-final for + * groovy, but not for java. + * + * @param mc self meta class + * @param fieldName name of field to make non-final + */ + public static void makeMutable(MetaClass mc, String fieldName) { + MetaProperty mp = mc.getMetaProperty(fieldName); + if (mp instanceof CachedField cachedField) { + ReflectionHelper.setFinal(cachedField.getCachedField(), false); + return; + } + GroovyLog.get().error("Failed to make member '{}' of class {} mutable, because no field was found!", fieldName, getName(mc)); + } + + public static String getName(MetaClass mc) { + ClassNode cn = mc.getClassNode(); + return cn == null ? "Unknown" : cn.getName(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/ReflectionHelper.java b/src/main/java/com/cleanroommc/groovyscript/helper/ReflectionHelper.java index 64539673d..71bd1c1b3 100644 --- a/src/main/java/com/cleanroommc/groovyscript/helper/ReflectionHelper.java +++ b/src/main/java/com/cleanroommc/groovyscript/helper/ReflectionHelper.java @@ -5,23 +5,91 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; public class ReflectionHelper { private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - private static Field modifiersField; + private static Field fieldModifiersField; + private static Field methodModifiersField; + private static MethodHandle fieldModifiersSetter; + private static MethodHandle methodModifiersSetter; - public static boolean setFinal(Field field, boolean isFinal) throws Throwable { + public static Field getFieldModifiersField() { + if (fieldModifiersField == null) { + try { + fieldModifiersField = Field.class.getDeclaredField("modifiers"); + } catch (NoSuchFieldException e) { + // something is very wrong if this crashes + throw new RuntimeException(e); + } + fieldModifiersField.setAccessible(true); + } + return fieldModifiersField; + } + + public static Field getMethodModifiersField() { + if (methodModifiersField == null) { + try { + methodModifiersField = Method.class.getDeclaredField("modifiers"); + } catch (NoSuchFieldException e) { + // something is very wrong if this crashes + throw new RuntimeException(e); + } + methodModifiersField.setAccessible(true); + } + return methodModifiersField; + } + + private static MethodHandle getFieldModifiersSetter() { + if (fieldModifiersSetter == null) { + try { + fieldModifiersSetter = LOOKUP.unreflectSetter(getFieldModifiersField()); + } catch (IllegalAccessException e) { + // something is very wrong if this crashes + throw new RuntimeException(e); + } + } + return fieldModifiersSetter; + } + + public static MethodHandle getMethodModifiersSetter() { + if (methodModifiersSetter == null) { + try { + methodModifiersSetter = LOOKUP.unreflectSetter(getMethodModifiersField()); + } catch (IllegalAccessException e) { + // something is very wrong if this crashes + throw new RuntimeException(e); + } + } + return methodModifiersSetter; + } + + public static void setModifiers(Field field, int modifiers) { + try { + getFieldModifiersSetter().invokeExact(field, modifiers); + } catch (Throwable e) { + // unlikely to crash + throw new RuntimeException(e); + } + } + + public static void setModifiers(Method method, int modifiers) { + try { + getMethodModifiersSetter().invokeExact(method, modifiers); + } catch (Throwable e) { + // unlikely to crash + throw new RuntimeException(e); + } + } + + public static boolean setFinal(Field field, boolean isFinal) { int m = field.getModifiers(); if (Modifier.isFinal(m) == isFinal) return false; - if (modifiersField == null) { - modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - } if (isFinal) m |= Modifier.FINAL; else m &= ~Modifier.FINAL; - LOOKUP.unreflectSetter(modifiersField).invokeExact(field, m); + setModifiers(field, m); return true; } @@ -77,4 +145,23 @@ public static Object getField(Object owner, String name) { return null; } } + + public static int makeModifiersPublic(int modifiers) { + if (!Modifier.isPublic(modifiers)) modifiers |= Modifier.PUBLIC; + if (Modifier.isProtected(modifiers)) modifiers &= ~Modifier.PROTECTED; + if (Modifier.isPrivate(modifiers)) modifiers &= ~Modifier.PRIVATE; + return modifiers; + } + + public static void makeFieldPublic(Field field) { + int mod = field.getModifiers(); + int newMod = makeModifiersPublic(mod); + if (mod != newMod) setModifiers(field, newMod); + } + + public static void makeMethodPublic(Method method) { + int mod = method.getModifiers(); + int newMod = makeModifiersPublic(mod); + if (mod != newMod) setModifiers(method, newMod); + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index 3545b98ab..03238bdb1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -10,13 +10,12 @@ import com.cleanroommc.groovyscript.event.ScriptRunEvent; import com.cleanroommc.groovyscript.helper.Alias; import com.cleanroommc.groovyscript.helper.GroovyHelper; +import com.cleanroommc.groovyscript.helper.MetaClassExpansion; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; +import com.cleanroommc.groovyscript.sandbox.expand.ExpansionHelper; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; -import groovy.lang.Binding; -import groovy.lang.Closure; -import groovy.lang.GroovyRuntimeException; -import groovy.lang.Script; +import groovy.lang.*; import groovy.util.ResourceException; import groovy.util.ScriptException; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; @@ -60,6 +59,8 @@ public GroovyScriptSandbox() { registerBinding("Log", GroovyLog.get()); registerBinding("EventManager", GroovyEventManager.INSTANCE); + ExpansionHelper.mixinClass(MetaClass.class, MetaClassExpansion.class); + getImportCustomizer().addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName()); getImportCustomizer().addImports( "net.minecraft.world.World",