Skip to content

Commit b4e8dd6

Browse files
authored
Add reflection helpers as metaclass expansion (#295)
* reflection helpers as metaclass expansion * move examples to custom/ & more
1 parent 4a6b552 commit b4e8dd6

File tree

6 files changed

+195
-27
lines changed

6 files changed

+195
-27
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
// no_run
3+
4+
def ore_iron = ore('ingotIron')
5+
def item_iron = item('minecraft:iron_ingot')
6+
log.info(item_iron in ore_iron) // true
7+
log.info(item_iron in item_iron) // true
8+
log.info(ore_iron in item_iron) // false
9+
log.info(item_iron << ore_iron) // true
10+
log.info((item_iron * 3) << ore_iron) // false
11+
log.info(ore_iron >> item_iron) // true
12+
log.info(ore_iron >> (item_iron * 3)) // false
13+
14+
file('config/').eachFile { file ->
15+
println file.path
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
// side: client
3+
4+
import net.minecraft.client.gui.GuiMainMenu
5+
import net.minecraftforge.client.event.GuiOpenEvent
6+
7+
// not a typo
8+
GuiMainMenu.metaClass.makePublic('minceraftRoll')
9+
GuiMainMenu.metaClass.makeMutable('minceraftRoll')
10+
11+
eventManager.listen(GuiOpenEvent) {
12+
if (gui instanceof GuiMainMenu) {
13+
// value is randomly set in constructor and checked if its smaller than 1e-4 during rendering
14+
// this forces the minceraft title to always activate
15+
gui.minceraftRoll = 0.00001
16+
}
17+
}

examples/postInit/custom/vanilla.groovy

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,6 @@ import net.minecraftforge.event.entity.living.EnderTeleportEvent
55
import net.minecraftforge.event.world.BlockEvent
66
import net.minecraft.util.text.TextComponentString
77

8-
/*
9-
def ore_iron = ore('ingotIron')
10-
def item_iron = item('minecraft:iron_ingot')
11-
log.info(item_iron in ore_iron) // true
12-
log.info(item_iron in item_iron) // true
13-
log.info(ore_iron in item_iron) // false
14-
log.info(item_iron << ore_iron) // true
15-
log.info((item_iron * 3) << ore_iron) // false
16-
log.info(ore_iron >> item_iron) // true
17-
log.info(ore_iron >> (item_iron * 3)) // false
18-
*/
19-
20-
/*file('config/').eachFile { file ->
21-
println file.path
22-
}*/
23-
248
for (var stack in mods.minecraft.allItems[5..12]) {
259
log.info stack
2610
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.cleanroommc.groovyscript.helper;
2+
3+
import com.cleanroommc.groovyscript.api.GroovyLog;
4+
import groovy.lang.MetaClass;
5+
import groovy.lang.MetaMethod;
6+
import groovy.lang.MetaProperty;
7+
import org.codehaus.groovy.ast.ClassNode;
8+
import org.codehaus.groovy.reflection.CachedField;
9+
import org.codehaus.groovy.reflection.CachedMethod;
10+
11+
public class MetaClassExpansion {
12+
13+
/**
14+
* 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
15+
* field/method from java and not some special cases like {@link groovy.lang.MetaBeanProperty} or other injected members via groovy.
16+
* Note that the {@link java.lang.reflect.Field Field} instance groovy stores and the one you get from {@link Class#getDeclaredField(String)} are different.
17+
* This means that calling this method will make the member only public for groovy, but not for java.
18+
*
19+
* @param mc self meta class
20+
* @param memberName name of members to make public
21+
*/
22+
public static void makePublic(MetaClass mc, String memberName) {
23+
boolean success = false;
24+
MetaProperty mp = mc.getMetaProperty(memberName);
25+
if (mp instanceof CachedField cachedField) {
26+
ReflectionHelper.makeFieldPublic(cachedField.getCachedField());
27+
success = true;
28+
}
29+
for (MetaMethod mm : mc.getMethods()) {
30+
if (memberName.equals(mm.getName()) && mm instanceof CachedMethod cachedMethod) {
31+
ReflectionHelper.makeMethodPublic(cachedMethod.getCachedMethod());
32+
success = true;
33+
}
34+
}
35+
if (!success) {
36+
GroovyLog.get().error("Failed to make member '{}' of class {} public, because no member was found!", memberName, getName(mc));
37+
}
38+
}
39+
40+
/**
41+
* Allows making a field with a specific name non-final. Does nothing if the field is already non-final.
42+
* 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
43+
* {@link groovy.lang.MetaBeanProperty} or other injected members via groovy. Note that the {@link java.lang.reflect.Field Field} instance groovy stores and
44+
* 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
45+
* groovy, but not for java.
46+
*
47+
* @param mc self meta class
48+
* @param fieldName name of field to make non-final
49+
*/
50+
public static void makeMutable(MetaClass mc, String fieldName) {
51+
MetaProperty mp = mc.getMetaProperty(fieldName);
52+
if (mp instanceof CachedField cachedField) {
53+
ReflectionHelper.setFinal(cachedField.getCachedField(), false);
54+
return;
55+
}
56+
GroovyLog.get().error("Failed to make member '{}' of class {} mutable, because no field was found!", fieldName, getName(mc));
57+
}
58+
59+
public static String getName(MetaClass mc) {
60+
ClassNode cn = mc.getClassNode();
61+
return cn == null ? "Unknown" : cn.getName();
62+
}
63+
}

src/main/java/com/cleanroommc/groovyscript/helper/ReflectionHelper.java

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,91 @@
55
import java.lang.invoke.MethodHandle;
66
import java.lang.invoke.MethodHandles;
77
import java.lang.reflect.Field;
8+
import java.lang.reflect.Method;
89
import java.lang.reflect.Modifier;
910

1011
public class ReflectionHelper {
1112

1213
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
13-
private static Field modifiersField;
14+
private static Field fieldModifiersField;
15+
private static Field methodModifiersField;
16+
private static MethodHandle fieldModifiersSetter;
17+
private static MethodHandle methodModifiersSetter;
1418

15-
public static boolean setFinal(Field field, boolean isFinal) throws Throwable {
19+
public static Field getFieldModifiersField() {
20+
if (fieldModifiersField == null) {
21+
try {
22+
fieldModifiersField = Field.class.getDeclaredField("modifiers");
23+
} catch (NoSuchFieldException e) {
24+
// something is very wrong if this crashes
25+
throw new RuntimeException(e);
26+
}
27+
fieldModifiersField.setAccessible(true);
28+
}
29+
return fieldModifiersField;
30+
}
31+
32+
public static Field getMethodModifiersField() {
33+
if (methodModifiersField == null) {
34+
try {
35+
methodModifiersField = Method.class.getDeclaredField("modifiers");
36+
} catch (NoSuchFieldException e) {
37+
// something is very wrong if this crashes
38+
throw new RuntimeException(e);
39+
}
40+
methodModifiersField.setAccessible(true);
41+
}
42+
return methodModifiersField;
43+
}
44+
45+
private static MethodHandle getFieldModifiersSetter() {
46+
if (fieldModifiersSetter == null) {
47+
try {
48+
fieldModifiersSetter = LOOKUP.unreflectSetter(getFieldModifiersField());
49+
} catch (IllegalAccessException e) {
50+
// something is very wrong if this crashes
51+
throw new RuntimeException(e);
52+
}
53+
}
54+
return fieldModifiersSetter;
55+
}
56+
57+
public static MethodHandle getMethodModifiersSetter() {
58+
if (methodModifiersSetter == null) {
59+
try {
60+
methodModifiersSetter = LOOKUP.unreflectSetter(getMethodModifiersField());
61+
} catch (IllegalAccessException e) {
62+
// something is very wrong if this crashes
63+
throw new RuntimeException(e);
64+
}
65+
}
66+
return methodModifiersSetter;
67+
}
68+
69+
public static void setModifiers(Field field, int modifiers) {
70+
try {
71+
getFieldModifiersSetter().invokeExact(field, modifiers);
72+
} catch (Throwable e) {
73+
// unlikely to crash
74+
throw new RuntimeException(e);
75+
}
76+
}
77+
78+
public static void setModifiers(Method method, int modifiers) {
79+
try {
80+
getMethodModifiersSetter().invokeExact(method, modifiers);
81+
} catch (Throwable e) {
82+
// unlikely to crash
83+
throw new RuntimeException(e);
84+
}
85+
}
86+
87+
public static boolean setFinal(Field field, boolean isFinal) {
1688
int m = field.getModifiers();
1789
if (Modifier.isFinal(m) == isFinal) return false;
18-
if (modifiersField == null) {
19-
modifiersField = Field.class.getDeclaredField("modifiers");
20-
modifiersField.setAccessible(true);
21-
}
2290
if (isFinal) m |= Modifier.FINAL;
2391
else m &= ~Modifier.FINAL;
24-
LOOKUP.unreflectSetter(modifiersField).invokeExact(field, m);
92+
setModifiers(field, m);
2593
return true;
2694
}
2795

@@ -77,4 +145,23 @@ public static Object getField(Object owner, String name) {
77145
return null;
78146
}
79147
}
148+
149+
public static int makeModifiersPublic(int modifiers) {
150+
if (!Modifier.isPublic(modifiers)) modifiers |= Modifier.PUBLIC;
151+
if (Modifier.isProtected(modifiers)) modifiers &= ~Modifier.PROTECTED;
152+
if (Modifier.isPrivate(modifiers)) modifiers &= ~Modifier.PRIVATE;
153+
return modifiers;
154+
}
155+
156+
public static void makeFieldPublic(Field field) {
157+
int mod = field.getModifiers();
158+
int newMod = makeModifiersPublic(mod);
159+
if (mod != newMod) setModifiers(field, newMod);
160+
}
161+
162+
public static void makeMethodPublic(Method method) {
163+
int mod = method.getModifiers();
164+
int newMod = makeModifiersPublic(mod);
165+
if (mod != newMod) setModifiers(method, newMod);
166+
}
80167
}

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
import com.cleanroommc.groovyscript.event.ScriptRunEvent;
1111
import com.cleanroommc.groovyscript.helper.Alias;
1212
import com.cleanroommc.groovyscript.helper.GroovyHelper;
13+
import com.cleanroommc.groovyscript.helper.MetaClassExpansion;
1314
import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager;
15+
import com.cleanroommc.groovyscript.sandbox.expand.ExpansionHelper;
1416
import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler;
1517
import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler;
16-
import groovy.lang.Binding;
17-
import groovy.lang.Closure;
18-
import groovy.lang.GroovyRuntimeException;
19-
import groovy.lang.Script;
18+
import groovy.lang.*;
2019
import groovy.util.ResourceException;
2120
import groovy.util.ScriptException;
2221
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@@ -60,6 +59,8 @@ public GroovyScriptSandbox() {
6059
registerBinding("Log", GroovyLog.get());
6160
registerBinding("EventManager", GroovyEventManager.INSTANCE);
6261

62+
ExpansionHelper.mixinClass(MetaClass.class, MetaClassExpansion.class);
63+
6364
getImportCustomizer().addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName());
6465
getImportCustomizer().addImports(
6566
"net.minecraft.world.World",

0 commit comments

Comments
 (0)