diff --git a/dependencies.gradle b/dependencies.gradle index 73bb9dd77..5d4900747 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -162,6 +162,7 @@ dependencies { exclude group: 'com.google.code.gson', module: 'gson' } + implementation("com.cleanroommc:modularui:2.5.1") { transitive false } api "codechicken:codechickenlib:3.2.3.358" // api "gregtech:gregtech:2.8.10-beta", { transitive false } diff --git a/examples/lib/ui.groovy b/examples/lib/ui.groovy new file mode 100644 index 000000000..1b807a244 --- /dev/null +++ b/examples/lib/ui.groovy @@ -0,0 +1,54 @@ + +import com.cleanroommc.modularui.ModularUI; +import com.cleanroommc.modularui.animation.Animator; +import com.cleanroommc.modularui.animation.IAnimator; +import com.cleanroommc.modularui.animation.Wait; +import com.cleanroommc.modularui.api.IThemeApi; +import com.cleanroommc.modularui.api.drawable.IDrawable; +import com.cleanroommc.modularui.api.drawable.IKey; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.drawable.GuiDraw; +import com.cleanroommc.modularui.drawable.GuiTextures; +import com.cleanroommc.modularui.drawable.ItemDrawable; +import com.cleanroommc.modularui.drawable.SpriteDrawable; +import com.cleanroommc.modularui.screen.CustomModularScreen; +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.RichTooltip; +import com.cleanroommc.modularui.screen.viewport.GuiContext; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.theme.WidgetTheme; +import com.cleanroommc.modularui.utils.Alignment; +import com.cleanroommc.modularui.utils.Color; +import com.cleanroommc.modularui.utils.GameObjectHelper; +import com.cleanroommc.modularui.utils.Interpolation; +import com.cleanroommc.modularui.utils.Interpolations; +import com.cleanroommc.modularui.utils.SpriteHelper; +import com.cleanroommc.modularui.utils.fakeworld.ArraySchema; +import com.cleanroommc.modularui.utils.fakeworld.FakeEntity; +import com.cleanroommc.modularui.utils.fakeworld.ISchema; +import com.cleanroommc.modularui.value.BoolValue; +import com.cleanroommc.modularui.value.StringValue; +import com.cleanroommc.modularui.widget.DraggableWidget; +import com.cleanroommc.modularui.widget.Widget; +import com.cleanroommc.modularui.widgets.ListWidget; +import com.cleanroommc.modularui.widgets.RichTextWidget; +import com.cleanroommc.modularui.widgets.SchemaWidget; +import com.cleanroommc.modularui.widgets.SortableListWidget; +import com.cleanroommc.modularui.widgets.TextWidget; +import com.cleanroommc.modularui.widgets.ToggleButton; +import com.cleanroommc.modularui.widgets.TransformWidget; +import com.cleanroommc.modularui.widgets.layout.Column; +import com.cleanroommc.modularui.widgets.layout.Flow; +import com.cleanroommc.modularui.widgets.layout.Grid; +import com.cleanroommc.modularui.widgets.layout.Row; +import com.cleanroommc.modularui.widgets.textfield.TextFieldWidget; + +class ui { + + static ModularPanel buildUI(ModularGuiContext context) { + return new ModularPanel("grs") + .size(100, 25) + .child(IKey.str("This UI was made with GroovyScript").asWidget().center()) + } + +} diff --git a/examples/mixins/PlayerListMixin.groovy b/examples/mixins/PlayerListMixin.groovy new file mode 100644 index 000000000..14087806f --- /dev/null +++ b/examples/mixins/PlayerListMixin.groovy @@ -0,0 +1,17 @@ + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.NetworkManager; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.server.management.PlayerList + +@Mixin(value=PlayerList.class, remap=false) +class PlayerListMixin { + + @Inject(method = "initializeConnectionToPlayer", at = @At(value = "INVOKE", + target = "Lnet/minecraftforge/fml/common/FMLCommonHandler;firePlayerLoggedIn(Lnet/minecraft/entity/player/EntityPlayer;)V")) + void init(NetworkManager netManager, EntityPlayerMP playerIn, NetHandlerPlayServer nethandlerplayserver, CallbackInfo ci) { + playerIn.sendMessage(new TextComponentString("Player " + playerIn.getName() + " logged in")); + } + +} diff --git a/examples/mixins/TestClassMixin.groovy b/examples/mixins/TestClassMixin.groovy new file mode 100644 index 000000000..01df653e0 --- /dev/null +++ b/examples/mixins/TestClassMixin.groovy @@ -0,0 +1,14 @@ + +import com.cleanroommc.groovyscript.TestClass +import com.cleanroommc.groovyscript.api.GroovyLog + +@Mixin(value = TestClass.class, remap=false) +class TestClassMixin { + + @Inject(method = "sayHello", at = @At("HEAD"), cancellable = true) + private static void sayBye(CallbackInfo ci) { + GroovyLog.get().info("Bye from TestClassMixin"); + ci.cancel(); + } + +} diff --git a/examples/mixins/TestGuisMixin.groovy b/examples/mixins/TestGuisMixin.groovy new file mode 100644 index 000000000..2c3c36119 --- /dev/null +++ b/examples/mixins/TestGuisMixin.groovy @@ -0,0 +1,20 @@ +// mods_loaded: modularui +// side: client + +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.test.TestGuis; +import lib.ui + +@Mixin(value = TestGuis.class, remap = false) +public abstract class TestGuisMixin { + + @Shadow + public abstract ModularPanel buildToggleGridListUI(ModularGuiContext context); + + @Inject(method = "buildUI", at = @At("HEAD"), cancellable = true) + public void buildUI(ModularGuiContext context, CallbackInfoReturnable cir) { + //cir.setReturnValue(buildToggleGridListUI(context)); + cir.setReturnValue(ui.buildUI(context)); + } +} diff --git a/examples/runConfig.json b/examples/runConfig.json index 0bc83cf70..4b77502fb 100644 --- a/examples/runConfig.json +++ b/examples/runConfig.json @@ -5,16 +5,8 @@ "version": "1.0.0", "debug": true, "loaders": { - "preInit": [ - "classes/", - "preInit/" - ], - "init": [ - "init/" - ], "postInit": [ - "postInit/", - "recipes/" + "test/" ] }, "packmode": { diff --git a/examples/test/main.groovy b/examples/test/main.groovy new file mode 100644 index 000000000..b8f81c8a3 --- /dev/null +++ b/examples/test/main.groovy @@ -0,0 +1,4 @@ + +import com.cleanroommc.groovyscript.TestClass + +TestClass.sayHello() diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index 436f80ccd..efb165844 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -97,6 +97,7 @@ public void onConstruction(FMLConstructionEvent event) { LOGGER.throwing(new IllegalStateException("Sandbox data should have been initialised by now, but isn't! Trying to initialize again.")); SandboxData.initialize((File) FMLInjectionData.data()[6], LOGGER); } + SandboxData.onLateInit(); MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(EventHandler.class); NetworkHandler.init(); @@ -105,6 +106,7 @@ public void onConstruction(FMLConstructionEvent event) { GroovyDeobfMapper.init(); LinkGeneratorHooks.init(); ReloadableRegistryManager.init(); + ((GroovyLogImpl) GroovyLog.get()).setPassedEarly(); GroovyScript.sandbox = new GroovyScriptSandbox(); ModSupport.INSTANCE.setup(event.getASMHarvestedData()); @@ -142,7 +144,6 @@ public void onRegisterItem(RegistryEvent.Register event) { @ApiStatus.Internal public static void initializeRunConfig(File minecraftHome) { SandboxData.initialize(minecraftHome, LOGGER); - reloadRunConfig(true); } @ApiStatus.Internal @@ -225,37 +226,7 @@ public static boolean isSandboxLoaded() { } public static RunConfig getRunConfig() { - return runConfig; - } - - @ApiStatus.Internal - public static void reloadRunConfig(boolean init) { - JsonElement element = JsonHelper.loadJson(getRunConfigFile()); - if (element == null || !element.isJsonObject()) element = new JsonObject(); - JsonObject json = element.getAsJsonObject(); - if (runConfig == null) { - if (!Files.exists(getRunConfigFile().toPath())) { - json = RunConfig.createDefaultJson(); - runConfig = createRunConfig(json); - } else { - runConfig = new RunConfig(json); - } - } - runConfig.reload(json, init); - } - - private static RunConfig createRunConfig(JsonObject json) { - JsonHelper.saveJson(getRunConfigFile(), json); - File main = new File(getScriptFile().getPath() + File.separator + "postInit" + File.separator + "main.groovy"); - if (!Files.exists(main.toPath())) { - try { - main.getParentFile().mkdirs(); - Files.write(main.toPath(), "\nlog.info('Hello World!')\n".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return new RunConfig(json); + return SandboxData.getRunConfig(); } public static void postScriptRunResult(ICommandSender sender, boolean onlyLogFails, boolean running, boolean packmode, long time) { diff --git a/src/main/java/com/cleanroommc/groovyscript/TestClass.java b/src/main/java/com/cleanroommc/groovyscript/TestClass.java index fc43ddc19..f66e20246 100644 --- a/src/main/java/com/cleanroommc/groovyscript/TestClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/TestClass.java @@ -1,39 +1,14 @@ package com.cleanroommc.groovyscript; -public class TestClass { - - private static int next = 20; - - private final Inner inner; - private final int id; +import com.cleanroommc.groovyscript.api.GroovyLog; - public TestClass(String name) { - this.inner = new Inner(name); - this.id = next++; - } - - public Inner getInner() { - return inner; - } +public class TestClass { - public int getId() { - return id; + static { + GroovyLog.get().info("Hello we are now initialising TestClass"); } - public class Inner { - - private final String name; - - public Inner(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public int getId() { - return id; - } + public static void sayHello() { + GroovyLog.get().info("Hello from TestClass"); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/api/GroovyLog.java b/src/main/java/com/cleanroommc/groovyscript/api/GroovyLog.java index a7c0709b5..4dc4ca7b5 100644 --- a/src/main/java/com/cleanroommc/groovyscript/api/GroovyLog.java +++ b/src/main/java/com/cleanroommc/groovyscript/api/GroovyLog.java @@ -260,7 +260,20 @@ default void errorMC(Object o) { * * @param throwable exception to log */ - void exception(String msg, Throwable throwable); + default void exception(String msg, Throwable throwable) { + exception(msg, throwable, false); + } + + /** + * Formats and logs an exception to this log AND Minecraft's log with a message.
+ * The log will be printed without formatting to Minecraft's log. + * Unnecessary lines that clutter the log will get removed before logging to this log.
+ * The exception will NOT be thrown! + * + * @param throwable exception to log + * @param doThrow true if the throwable should be thrown after logged to groovy log + */ + void exception(String msg, Throwable throwable, boolean doThrow); /** * Formats a {@link String} and arguments according to the defined rules. diff --git a/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java b/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java index e700eda22..02ab44735 100644 --- a/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java +++ b/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java @@ -65,7 +65,6 @@ public GSCommand() { sender.sendMessage(new TextComponentString("Applied the default GameRules to the current world.")); })); - addSubcommand(new SimpleCommand("wiki", (server, sender, args) -> sender.sendMessage(getTextForUrl("GroovyScript wiki", "Click to open wiki in browser", new TextComponentString("https://cleanroommc.com/groovy-script/"))), "doc", "docs", "documentation")); addSubcommand(new SimpleCommand("generateWiki", (server, sender, args) -> { diff --git a/src/main/java/com/cleanroommc/groovyscript/core/AbstractMixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/AbstractMixinPlugin.java new file mode 100644 index 000000000..b4b31a123 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/AbstractMixinPlugin.java @@ -0,0 +1,45 @@ +package com.cleanroommc.groovyscript.core; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager; +import com.cleanroommc.groovyscript.sandbox.security.SandboxSecurityException; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public abstract class AbstractMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return false; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) {} + + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + if (!GroovySecurityManager.INSTANCE.isValid(targetClass, targetClassName)) { + GroovyLog.get() + .exception( + "An exception while applying a mixin occurred.", + new SandboxSecurityException("Can't mixin into class '" + targetClassName + "', since it is blacklisted for groovy!"), + true); + } + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/EarlyMixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/EarlyMixinPlugin.java new file mode 100644 index 000000000..11b4b3d8d --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/EarlyMixinPlugin.java @@ -0,0 +1,15 @@ +package com.cleanroommc.groovyscript.core; + +import com.cleanroommc.groovyscript.sandbox.MixinSandbox; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public class EarlyMixinPlugin extends AbstractMixinPlugin { + + @Override + public List getMixins() { + return MixinSandbox.getEarlyMixinClasses(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java index 3ee549fe1..407e0d996 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java @@ -1,5 +1,6 @@ package com.cleanroommc.groovyscript.core; +import com.cleanroommc.groovyscript.sandbox.MixinSandbox; import com.cleanroommc.groovyscript.sandbox.SandboxData; import com.google.common.collect.ImmutableList; import net.minecraftforge.common.ForgeVersion; @@ -42,6 +43,11 @@ public void injectData(Map data) { source = (File) data.getOrDefault("coremodLocation", null); SandboxData.initialize((File) FMLInjectionData.data()[6], LOG); SideOnlyConfig.init(); + try { + MixinSandbox.loadEarlyMixins(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override @@ -51,6 +57,6 @@ public String getAccessTransformerClass() { @Override public List getMixinConfigs() { - return ImmutableList.of("mixin.groovyscript.json"); + return ImmutableList.of("mixin.groovyscript.groovy.json", "mixin.groovyscript.json"); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java index f3bcbba4b..5566192f0 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java @@ -1,6 +1,7 @@ package com.cleanroommc.groovyscript.core; import com.cleanroommc.groovyscript.compat.mods.ic2.IC2; +import com.cleanroommc.groovyscript.sandbox.MixinSandbox; import com.google.common.collect.ImmutableList; import net.minecraftforge.fml.common.Loader; import zone.rong.mixinbooter.ILateMixinLoader; @@ -11,6 +12,7 @@ public class LateMixin implements ILateMixinLoader { public static final List modMixins = ImmutableList.of( + "custom.late", "advancedmortars", "appliedenergistics2", "armorplus", @@ -49,6 +51,7 @@ public class LateMixin implements ILateMixinLoader { @Override public List getMixinConfigs() { + MixinSandbox.loadLateMixins(); return modMixins.stream().map(mod -> "mixin.groovyscript." + mod + ".json").collect(Collectors.toList()); } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/LateMixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/LateMixinPlugin.java new file mode 100644 index 000000000..f98cbff18 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/LateMixinPlugin.java @@ -0,0 +1,15 @@ +package com.cleanroommc.groovyscript.core; + +import com.cleanroommc.groovyscript.sandbox.MixinSandbox; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public class LateMixinPlugin extends AbstractMixinPlugin { + + @Override + public List getMixins() { + return MixinSandbox.getLateMixinClasses(); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java new file mode 100644 index 000000000..8607f34d5 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java @@ -0,0 +1,58 @@ +package com.cleanroommc.groovyscript.core; + +import com.google.common.collect.ForwardingMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * Stolen from ZenUtils. + * Github Link + * + * @author youyihj + */ +/* + Let's explain why this class exists. + We need to inject bytecodes compiled by zs into the LaunchClassLoader's resource cache to load them by LCL. + Basically, the resource cache is a ConcurrentHashMap. Its contents will not be lost and will be preserved for a long time. + It is fine for us, but introduces a lot of RAM waste. + So some mods change the cache to guava's Cache to save RAM but lost our bytecodes. + VintageFix sets the cache that contents will be expired after 1 minute, which produces issue for us. + Loliasm sets the cache that contents are weak references. But our bytecodes are strongly referenced in ZenModule and never garbage collected. + But the phase of them two setting up the cache is different. VintageFix sets the cache very early (FMLLoadingPlugin), while Loliasm sets the cache "very" late (FMLLoadCompleteEvent). + VintageFix's optimization is still important when loading textures, sounds, etc. We can not break both of them. + + This class is a wrapper for VintageFix's cache, concatenated with our bytecodes. Loliasm will set its cache later, but like talking before, our bytecodes won't be lost in its cache. +*/ +public class LaunchClassLoaderResourceCache extends ForwardingMap { + + private final Map delegate; + + // immutable to thread-safe // groovyscript: we need to modify the map when loading late mixins + private final Map injected; + + public LaunchClassLoaderResourceCache(Map delegate, Map injected) { + this.delegate = delegate; + this.injected = injected; + } + + @Override + public boolean containsKey(@Nullable Object key) { + return super.containsKey(key) || injected.containsKey(key); + } + + @Override + public byte[] get(@Nullable Object key) { + byte[] bytes = super.get(key); + if (bytes == null) { + bytes = injected.get(key); + } + return bytes; + } + + @Override + protected Map delegate() { + return delegate; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java index 3bd445c91..ccfdacc69 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ModuleNodeMixin.java @@ -38,6 +38,7 @@ public void init(SourceUnit context, CallbackInfo ci) { // inject correct package declaration into script String packageName = rel.substring(0, i).replace('/', '.') + '.'; this.packageNode = new PackageNode(packageName); + this.packageNode.setSynthetic(true); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/PairList.java b/src/main/java/com/cleanroommc/groovyscript/helper/PairList.java new file mode 100644 index 000000000..6d9aa7aad --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/helper/PairList.java @@ -0,0 +1,84 @@ +package com.cleanroommc.groovyscript.helper; + +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Iterables; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class PairList implements Iterable> { + + private final List l1 = new ArrayList<>(); + private final List l2 = new ArrayList<>(); + + public void add(T1 t1, T2 t2) { + this.l1.add(t1); + this.l2.add(t2); + } + + public int size() { + return this.l1.size(); + } + + public boolean isEmpty() { + return this.l1.isEmpty(); + } + + public T1 getLeft(int index) { + return this.l1.get(index); + } + + public T2 getRight(int index) { + return this.l2.get(index); + } + + public Pair get(int index) { + return Pair.of(getLeft(index), getRight(index)); + } + + public Iterable getLeftIterable() { + return () -> new AbstractIterator<>() { + + private final Iterator it = PairList.this.l1.iterator(); + + @Override + protected T1 computeNext() { + return it.hasNext() ? it.next() : endOfData(); + } + }; + } + + public Iterable getRightIterable() { + return () -> new AbstractIterator<>() { + + private final Iterator it = PairList.this.l2.iterator(); + + @Override + protected T2 computeNext() { + return it.hasNext() ? it.next() : endOfData(); + } + }; + } + + @NotNull + @Override + public Iterator> iterator() { + return new AbstractIterator<>() { + + private final MutablePair pair = MutablePair.of(null, null); + private int index = -1; + + @Override + protected Pair computeNext() { + if (++this.index == size()) return endOfData(); + this.pair.setLeft(getLeft(this.index)); + this.pair.setRight(getRight(this.index)); + return this.pair; + } + }; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/registry/ReloadableRegistryManager.java b/src/main/java/com/cleanroommc/groovyscript/registry/ReloadableRegistryManager.java index 2d7e68c63..f4a5115f6 100644 --- a/src/main/java/com/cleanroommc/groovyscript/registry/ReloadableRegistryManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/registry/ReloadableRegistryManager.java @@ -9,6 +9,7 @@ import com.cleanroommc.groovyscript.compat.mods.GroovyPropertyContainer; import com.cleanroommc.groovyscript.compat.mods.ModSupport; import com.cleanroommc.groovyscript.core.mixin.jei.JeiProxyAccessor; +import com.cleanroommc.groovyscript.sandbox.SandboxData; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import mezz.jei.Internal; import mezz.jei.JustEnoughItems; @@ -74,7 +75,7 @@ public static void init() { @ApiStatus.Internal public static void onReload() { - GroovyScript.reloadRunConfig(false); + SandboxData.reloadRunConfig(false); ModSupport.getAllContainers() .stream() .filter(GroovyContainer::isLoaded) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java new file mode 100644 index 000000000..b8ff1ecb0 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java @@ -0,0 +1,338 @@ +package com.cleanroommc.groovyscript.sandbox; + +import com.cleanroommc.groovyscript.GroovyScript; +import com.cleanroommc.groovyscript.api.GroovyBlacklist; +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.api.INamed; +import com.cleanroommc.groovyscript.helper.Alias; +import com.cleanroommc.groovyscript.sandbox.engine.CompiledScript; +import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; +import groovy.lang.Binding; +import groovy.lang.Closure; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.Script; +import groovy.util.ResourceException; +import groovy.util.ScriptException; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.apache.groovy.internal.util.UncheckedThrow; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.InvokerInvocationException; +import org.jetbrains.annotations.ApiStatus; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public abstract class AbstractGroovySandbox { + + private final ScriptEngine engine; + + private String currentScript; + private LoadStage currentLoadStage; + + private final ThreadLocal running = ThreadLocal.withInitial(() -> false); + private final Map bindings = new Object2ObjectOpenHashMap<>(); + private final ImportCustomizer importCustomizer = new ImportCustomizer(); + private final Map, AtomicInteger> storedExceptions = new Object2ObjectOpenHashMap<>(); + + protected long compileTime; + protected long runTime; + + public AbstractGroovySandbox() { + CompilerConfiguration config = new CompilerConfiguration(); + initConfig(config); + this.engine = createEngine(config); + } + + protected abstract ScriptEngine createEngine(CompilerConfiguration config); + + protected Binding createBindings() { + Binding binding = new Binding(this.bindings); + postInitBindings(binding); + return binding; + } + + public Map getGlobals() { + return this.bindings; + } + + @Deprecated + public void registerBinding(String name, Object obj) { + registerGlobal(name, obj); + } + + @Deprecated + public void registerBinding(INamed named) { + registerGlobal(named); + } + + public void registerGlobal(String name, Object obj) { + Objects.requireNonNull(name); + Objects.requireNonNull(obj); + for (String alias : Alias.generateOf(name)) { + bindings.put(alias, obj); + } + } + + public void registerGlobal(INamed named) { + Objects.requireNonNull(named); + for (String alias : named.getAliases()) { + bindings.put(alias, named); + } + } + + public abstract boolean canRunInStage(LoadStage stage); + + public void run(LoadStage currentLoadStage) { + if (!canRunInStage(currentLoadStage)) { + throw new IllegalArgumentException("The current sandbox can not run in load stage " + currentLoadStage); + } + this.currentLoadStage = Objects.requireNonNull(currentLoadStage); + try { + load(); + } catch (IOException | ScriptException | ResourceException e) { + GroovyLog.get().exception("An exception occurred while trying to run groovy code! This is might be a internal groovy issue.", e); + } catch (Throwable t) { + GroovyLog.get().exception(t); + } finally { + GroovyLog.get().infoMC("Groovy scripts took {}ms to compile and {}ms to run in {}.", this.compileTime, this.runTime, currentLoadStage.getName()); + this.currentLoadStage = null; + if (currentLoadStage == LoadStage.POST_INIT) { + engine.writeIndex(); + } + } + } + + protected void runScript(Script script) throws Throwable { + GroovyLog.get().info(" - running script {}", script.getClass().getName()); + setCurrentScript(script.getClass().getName()); + try { + script.run(); + } finally { + setCurrentScript(null); + } + } + + protected void runClass(Class script) throws Throwable { + GroovyLog.get().info(" - loading class {}", script.getName()); + setCurrentScript(script.getName()); + try { + // $getLookup is present on all groovy created classes + // call it cause the class to be initialised + Method m = script.getMethod("$getLookup"); + m.invoke(null); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + GroovyLog.get().errorMC("Error initialising class '{}'", script); + } finally { + setCurrentScript(null); + } + } + + public void checkSyntax() { + Binding binding = createBindings(); + Set executedClasses = new ObjectOpenHashSet<>(); + + for (LoadStage loadStage : LoadStage.getLoadStages()) { + GroovyLog.get().info("Checking syntax in loader '{}'", this.currentLoadStage); + this.currentLoadStage = loadStage; + try { + load(binding, executedClasses, false); + } catch (Throwable e) { + GroovyLog.get().exception(e); + } + } + } + + @ApiStatus.Internal + public T runClosure(Closure closure, Object... args) { + boolean wasRunning = isRunning(); + if (!wasRunning) startRunning(); + T result = null; + try { + result = runClosureInternal(closure, args); + } catch (Throwable t) { + List stackTrace = Arrays.asList(t.getStackTrace()); + AtomicInteger counter = this.storedExceptions.get(stackTrace); + if (counter == null) { + GroovyLog.get().exception("An exception occurred while running a closure at least once!", t); + this.storedExceptions.put(stackTrace, new AtomicInteger(1)); + UncheckedThrow.rethrow(t); + return null; // unreachable statement + } else { + counter.getAndIncrement(); + } + } finally { + if (!wasRunning) stopRunning(); + } + return result; + } + + @GroovyBlacklist + private static T runClosureInternal(Closure closure, Object[] args) throws Throwable { + // original Closure.call(Object... arguments) code + try { + //noinspection unchecked + return (T) closure.getMetaClass().invokeMethod(closure, "doCall", args); + } catch (InvokerInvocationException e) { + throw e.getCause(); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw e; + } else { + throw new GroovyRuntimeException(e.getMessage(), e); + } + } + } + + private void load() throws Throwable { + preRun(); + + Binding binding = createBindings(); + Set executedClasses = new ObjectOpenHashSet<>(); + + this.running.set(true); + try { + load(binding, executedClasses, true); + } finally { + this.running.set(false); + postRun(); + setCurrentScript(null); + } + } + + protected void load(Binding binding, Set executedClasses, boolean run) throws Throwable { + this.compileTime = 0L; + this.runTime = 0L; + // now run all script files + loadScripts(binding, executedClasses, run); + } + + protected void loadScripts(Binding binding, Set executedClasses, boolean run) throws Throwable { + FileUtil.cleanScriptPathWarnedCache(); + Collection files = getScriptFiles(); + if (files.isEmpty()) return; + List scripts = this.engine.findScripts(files); + if (scripts.isEmpty()) return; + for (CompiledScript compiledScript : scripts) { + if (!executedClasses.contains(compiledScript.getPath())) { + loadScript(compiledScript, binding, run); + Class clz = compiledScript.getScriptClass(); + if (!compiledScript.preprocessorCheckFailed() && clz != null && isClassScript(clz)) { + executedClasses.add(compiledScript.getPath()); + } + } + } + } + + protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) throws Throwable { + long t = System.currentTimeMillis(); + this.engine.loadScript(compiledScript); + this.compileTime += System.currentTimeMillis() - t; + if (compiledScript.preprocessorCheckFailed()) return; + if (compiledScript.getScriptClass() == null) { + GroovyLog.get().errorMC("Error loading script {}", compiledScript.getPath()); + return; + } + if (!isClassScript(compiledScript.getScriptClass())) { + // script is a class + if (run && shouldRunFile(compiledScript.getPath())) { + t = System.currentTimeMillis(); + runClass(compiledScript.getScriptClass()); + this.runTime += System.currentTimeMillis() - t; + } + return; + } + if (run && shouldRunFile(compiledScript.getPath())) { + Script script = InvokerHelper.createScript(compiledScript.getScriptClass(), binding); + t = System.currentTimeMillis(); + runScript(script); + this.runTime += System.currentTimeMillis() - t; + } + } + + protected void startRunning() { + this.running.set(true); + } + + protected void stopRunning() { + this.running.set(false); + } + + @ApiStatus.OverrideOnly + protected void postInitBindings(Binding binding) { + binding.setProperty("out", GroovyLog.get().getWriter()); + binding.setVariable("globals", getBindings()); + } + + @ApiStatus.OverrideOnly + protected void initConfig(CompilerConfiguration config) { + config.addCompilationCustomizers(getImportCustomizer()); + } + + @ApiStatus.OverrideOnly + protected void preRun() { + if (ScriptEngine.DELETE_CACHE_ON_RUN) this.engine.deleteScriptCache(); + } + + @ApiStatus.OverrideOnly + protected boolean shouldRunFile(String file) { + return true; + } + + @ApiStatus.OverrideOnly + protected void postRun() {} + + public File getScriptRoot() { + return getEngine().getScriptRoot(); + } + + public Collection getScriptFiles() { + return GroovyScript.getRunConfig().getSortedFiles(getScriptRoot(), this.currentLoadStage.getName()); + } + + public boolean isRunning() { + return this.running.get(); + } + + public Map getBindings() { + return bindings; + } + + public ImportCustomizer getImportCustomizer() { + return importCustomizer; + } + + public ScriptEngine getEngine() { + return engine; + } + + public String getCurrentScript() { + return currentScript; + } + + protected void setCurrentScript(String currentScript) { + this.currentScript = currentScript; + } + + public LoadStage getCurrentLoader() { + return currentLoadStage; + } + + public long getLastCompileTime() { + return compileTime; + } + + public long getLastRunTime() { + return runTime; + } + + public static boolean isClassScript(Class clazz) { + return Script.class.isAssignableFrom(clazz); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/ActualSide.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/ActualSide.java new file mode 100644 index 000000000..89a8af6b1 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/ActualSide.java @@ -0,0 +1,53 @@ +package com.cleanroommc.groovyscript.sandbox; + +import net.minecraftforge.fml.relauncher.Side; + +public enum ActualSide { + + PHYSICAL_CLIENT("PH_CLIENT", "CLIENT", true, true), + PHYSICAL_SERVER("PH_SERVER", "SERVER", true, false), + LOGICAL_CLIENT("LO_CLIENT", "CLIENT", false, true), + LOGICAL_SERVER("LO_SERVER", "SERVER", false, false); + + private final String name, shortName; + private final boolean physical, client; + + ActualSide(String name, String shortName, boolean physical, boolean client) { + this.name = name; + this.shortName = shortName; + this.physical = physical; + this.client = client; + } + + public String getName() { + return name; + } + + public String getShortName() { + return shortName; + } + + public boolean isClient() { + return client; + } + + public boolean isServer() { + return !client; + } + + public boolean isPhysical() { + return physical; + } + + public boolean isLogical() { + return !physical; + } + + public static ActualSide ofPhysicalSide(Side side) { + return side.isClient() ? PHYSICAL_CLIENT : PHYSICAL_SERVER; + } + + public static ActualSide ofLogicalSide(Side side) { + return side.isClient() ? LOGICAL_CLIENT : LOGICAL_SERVER; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java index 020e1d4d7..aedd373b1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java @@ -3,15 +3,15 @@ import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.GroovyLog; -import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.ModContainer; -import net.minecraftforge.fml.relauncher.FMLInjectionData; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; +import net.minecraftforge.fml.relauncher.Side; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.intellij.lang.annotations.Flow; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,14 +39,19 @@ public class GroovyLogImpl implements GroovyLog { private PrintWriter printWriter; private final DateFormat timeFormat = new SimpleDateFormat("[HH:mm:ss]"); private List errors = new ArrayList<>(); + private boolean passedEarly = false; private GroovyLogImpl() { - File minecraftHome = (File) FMLInjectionData.data()[6]; - File logFile = new File(minecraftHome, "logs" + File.separator + getLogFileName()); + File logFile = new File(SandboxData.getMinecraftHome(), "logs" + File.separator + getLogFileName()); this.logFilePath = logFile.toPath(); this.printWriter = setupLog(logFile); } + @ApiStatus.Internal + public void setPassedEarly() { + this.passedEarly = true; + } + public void cleanLog() { this.printWriter = setupLog(this.logFilePath.toFile()); } @@ -255,14 +260,8 @@ public void exception(Throwable throwable) { exception("An exception occurred while running scripts.", throwable); } - /** - * Logs an exception to the groovy log AND Minecraft's log. It does NOT throw the exception! The stacktrace for the groovy log will be - * stripped for better readability. - * - * @param throwable exception - */ @Override - public void exception(String msg, Throwable throwable) { + public void exception(String msg, Throwable throwable, boolean doThrow) { String throwableMsg = throwable.toString(); this.errors.add(throwableMsg); msg += " Look at latest.log for a full stacktrace:"; @@ -278,7 +277,12 @@ public void exception(String msg, Throwable throwable) { } } GroovyScript.LOGGER.error(msg); - GroovyScript.LOGGER.throwing(throwable); + if (doThrow) { + if (throwable instanceof RuntimeException e) throw e; + throw new RuntimeException(throwable); + } else { + GroovyScript.LOGGER.throwing(throwable); + } } private List prepareStackTrace(StackTraceElement[] stackTrace) { @@ -299,7 +303,15 @@ private List prepareStackTrace(StackTraceElement[] stackTrace) { } private String formatLine(String level, String msg) { - return timeFormat.format(new Date()) + (FMLCommonHandler.instance().getEffectiveSide().isClient() ? " [CLIENT/" : " [SERVER/") + level + "]" + " [" + getSource() + "]: " + msg; + return timeFormat.format(new Date()) + " [" + getSide() + "/" + level + "]" + " [" + getSource() + "]: " + msg; + } + + private String getSide() { + // if we load FMLCommonHandler to early it will cause class loading issues with other classes + // guess how long it took to figure this out + // if we are in early stage use fallback side which is available early, but might be inaccurate + ActualSide side = SandboxData.getLogicalSide(); + return side.isPhysical() ? side.getName() : side.getShortName(); } private String getSource() { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index 2a56b6223..859598d74 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -1,63 +1,34 @@ package com.cleanroommc.groovyscript.sandbox; -import com.cleanroommc.groovyscript.GroovyScript; -import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.GroovyLog; -import com.cleanroommc.groovyscript.api.INamed; import com.cleanroommc.groovyscript.compat.mods.ModSupport; import com.cleanroommc.groovyscript.event.GroovyEventManager; import com.cleanroommc.groovyscript.event.GroovyReloadEvent; 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.engine.ScriptEngine; import com.cleanroommc.groovyscript.sandbox.expand.ExpansionHelper; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; import groovy.lang.*; -import groovy.util.ResourceException; -import groovy.util.ScriptException; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.util.math.MathHelper; import net.minecraftforge.common.MinecraftForge; -import org.apache.groovy.internal.util.UncheckedThrow; import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.customizers.ImportCustomizer; -import org.codehaus.groovy.runtime.InvokerHelper; -import org.codehaus.groovy.runtime.InvokerInvocationException; -import org.jetbrains.annotations.ApiStatus; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; +public class GroovyScriptSandbox extends AbstractGroovySandbox { -public class GroovyScriptSandbox { - - private final CustomGroovyScriptEngine engine; - - private String currentScript; - private LoadStage currentLoadStage; - - private final ThreadLocal running = ThreadLocal.withInitial(() -> false); - private final Map bindings = new Object2ObjectOpenHashMap<>(); - private final ImportCustomizer importCustomizer = new ImportCustomizer(); - private final Map, AtomicInteger> storedExceptions = new Object2ObjectOpenHashMap<>(); - - private long compileTime; - private long runTime; + @Override + protected ScriptEngine createEngine(CompilerConfiguration config) { + return new ScriptEngine(SandboxData.getRootUrls(), SandboxData.getStandardScriptCachePath(), SandboxData.getScriptFile(), config); + } - public GroovyScriptSandbox() { - CompilerConfiguration config = new CompilerConfiguration(); - initEngine(config); - this.engine = new CustomGroovyScriptEngine(SandboxData.getRootUrls(), SandboxData.getCachePath(), SandboxData.getScriptFile(), config); - registerBinding("Mods", ModSupport.INSTANCE); - registerBinding("Log", GroovyLog.get()); - registerBinding("EventManager", GroovyEventManager.INSTANCE); + @Override + protected void initConfig(CompilerConfiguration config) { + registerGlobal("Mods", ModSupport.INSTANCE); + registerGlobal("Log", GroovyLog.get()); + registerGlobal("EventManager", GroovyEventManager.INSTANCE); ExpansionHelper.mixinClass(MetaClass.class, MetaClassExpansion.class); @@ -94,273 +65,41 @@ public GroovyScriptSandbox() { "com.cleanroommc.groovyscript.event.EventBusType", "net.minecraftforge.fml.relauncher.Side", "net.minecraftforge.fml.relauncher.SideOnly"); - } - - protected Binding createBindings() { - Binding binding = new Binding(this.bindings); - postInitBindings(binding); - return binding; - } - - public void registerBinding(String name, Object obj) { - Objects.requireNonNull(name); - Objects.requireNonNull(obj); - for (String alias : Alias.generateOf(name)) { - bindings.put(alias, obj); - } - } - - public void registerBinding(INamed named) { - Objects.requireNonNull(named); - for (String alias : named.getAliases()) { - bindings.put(alias, named); - } - } - - public void run(LoadStage currentLoadStage) { - this.currentLoadStage = Objects.requireNonNull(currentLoadStage); - try { - load(); - } catch (IOException | ScriptException | ResourceException e) { - GroovyLog.get().exception("An exception occurred while trying to run groovy code! This is might be a internal groovy issue.", e); - } catch (Throwable t) { - GroovyLog.get().exception(t); - } finally { - GroovyLog.get().infoMC("Groovy scripts took {}ms to compile and {}ms to run in {}.", this.compileTime, this.runTime, currentLoadStage.getName()); - this.currentLoadStage = null; - if (currentLoadStage == LoadStage.POST_INIT) { - engine.writeIndex(); - } - } - } - - protected void runScript(Script script) { - GroovyLog.get().info(" - running script {}", script.getClass().getName()); - setCurrentScript(script.getClass().getName()); - try { - script.run(); - } finally { - setCurrentScript(null); - } - } - - protected void runClass(Class script) { - GroovyLog.get().info(" - loading class {}", script.getName()); - setCurrentScript(script.getName()); - try { - // $getLookup is present on all groovy created classes - // call it cause the class to be initialised - Method m = script.getMethod("$getLookup"); - m.invoke(null); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - GroovyLog.get().errorMC("Error initialising class '{}'", script); - } finally { - setCurrentScript(null); - } - } - - public void checkSyntax() { - Binding binding = createBindings(); - Set executedClasses = new ObjectOpenHashSet<>(); - - for (LoadStage loadStage : LoadStage.getLoadStages()) { - GroovyLog.get().info("Checking syntax in loader '{}'", this.currentLoadStage); - this.currentLoadStage = loadStage; - load(binding, executedClasses, false); - } - } - - @ApiStatus.Internal - public T runClosure(Closure closure, Object... args) { - boolean wasRunning = isRunning(); - if (!wasRunning) startRunning(); - T result = null; - try { - result = runClosureInternal(closure, args); - } catch (Throwable t) { - List stackTrace = Arrays.asList(t.getStackTrace()); - AtomicInteger counter = this.storedExceptions.get(stackTrace); - if (counter == null) { - GroovyLog.get().exception("An exception occurred while running a closure at least once!", t); - this.storedExceptions.put(stackTrace, new AtomicInteger(1)); - UncheckedThrow.rethrow(t); - return null; // unreachable statement - } else { - counter.getAndIncrement(); - } - } finally { - if (!wasRunning) stopRunning(); - } - return result; - } - - @GroovyBlacklist - private static T runClosureInternal(Closure closure, Object[] args) throws Throwable { - // original Closure.call(Object... arguments) code - try { - //noinspection unchecked - return (T) closure.getMetaClass().invokeMethod(closure, "doCall", args); - } catch (InvokerInvocationException e) { - throw e.getCause(); - } catch (Exception e) { - if (e instanceof RuntimeException) { - throw e; - } else { - throw new GroovyRuntimeException(e.getMessage(), e); - } - } - } - - private void load() throws Exception { - preRun(); - - Binding binding = createBindings(); - Set executedClasses = new ObjectOpenHashSet<>(); - - this.running.set(true); - try { - load(binding, executedClasses, true); - } finally { - this.running.set(false); - postRun(); - setCurrentScript(null); - } - } - - protected void load(Binding binding, Set executedClasses, boolean run) { - this.compileTime = 0L; - this.runTime = 0L; - // now run all script files - loadScripts(binding, executedClasses, run); - } - - protected void loadScripts(Binding binding, Set executedClasses, boolean run) { - FileUtil.cleanScriptPathWarnedCache(); - for (CompiledScript compiledScript : this.engine.findScripts(getScriptFiles())) { - if (!executedClasses.contains(compiledScript.path)) { - long t = System.currentTimeMillis(); - this.engine.loadScript(compiledScript); - this.compileTime += System.currentTimeMillis() - t; - if (compiledScript.preprocessorCheckFailed()) continue; - if (compiledScript.clazz == null) { - GroovyLog.get().errorMC("Error loading script {}", compiledScript.path); - continue; - } - if (compiledScript.clazz.getSuperclass() != Script.class) { - // script is a class - if (run && shouldRunFile(compiledScript.path)) { - t = System.currentTimeMillis(); - runClass(compiledScript.clazz); - this.runTime += System.currentTimeMillis() - t; - } - executedClasses.add(compiledScript.path); - continue; - } - if (run && shouldRunFile(compiledScript.path)) { - Script script = InvokerHelper.createScript(compiledScript.clazz, binding); - t = System.currentTimeMillis(); - runScript(script); - this.runTime += System.currentTimeMillis() - t; - } - } - } - } - - protected void startRunning() { - this.running.set(true); - } - - protected void stopRunning() { - this.running.set(false); - } - - @ApiStatus.OverrideOnly - protected void postInitBindings(Binding binding) { - binding.setProperty("out", GroovyLog.get().getWriter()); - binding.setVariable("globals", getBindings()); - } - - @ApiStatus.OverrideOnly - protected void initEngine(CompilerConfiguration config) { - config.addCompilationCustomizers(this.importCustomizer); + super.initConfig(config); config.addCompilationCustomizers(new GroovyScriptCompiler()); config.addCompilationCustomizers(new GroovyScriptEarlyCompiler()); } - @ApiStatus.OverrideOnly + @Override + public boolean canRunInStage(LoadStage stage) { + return !stage.isMixin(); + } + + @Override protected void preRun() { - if (CustomGroovyScriptEngine.DELETE_CACHE_ON_RUN) this.engine.deleteScriptCache(); + super.preRun(); // first clear all added events GroovyEventManager.INSTANCE.reset(); - if (this.currentLoadStage.isReloadable() && !ReloadableRegistryManager.isFirstLoad()) { + if (getCurrentLoader().isReloadable() && !ReloadableRegistryManager.isFirstLoad()) { // if this is not the first time this load stage is executed, reload all virtual registries ReloadableRegistryManager.onReload(); // invoke reload event MinecraftForge.EVENT_BUS.post(new GroovyReloadEvent()); } - GroovyLog.get().infoMC("Running scripts in loader '{}'", this.currentLoadStage); - //this.engine.prepareEngine(this.currentLoadStage); + GroovyLog.get().infoMC("Running scripts in loader '{}'", getCurrentLoader()); + // this.engine.prepareEngine(this.currentLoadStage); // and finally invoke pre script run event - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(this.currentLoadStage)); + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(getCurrentLoader())); } - @ApiStatus.OverrideOnly - protected boolean shouldRunFile(String file) { - return true; - } - - @ApiStatus.OverrideOnly + @Override protected void postRun() { - if (this.currentLoadStage == LoadStage.POST_INIT) { + if (getCurrentLoader() == LoadStage.POST_INIT) { ReloadableRegistryManager.afterScriptRun(); } - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(this.currentLoadStage)); - if (this.currentLoadStage == LoadStage.POST_INIT && ReloadableRegistryManager.isFirstLoad()) { + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(getCurrentLoader())); + if (getCurrentLoader() == LoadStage.POST_INIT && ReloadableRegistryManager.isFirstLoad()) { ReloadableRegistryManager.setLoaded(); } } - - public File getScriptRoot() { - return SandboxData.getScriptFile(); - } - - public Collection getScriptFiles() { - return GroovyScript.getRunConfig().getSortedFiles(getScriptRoot(), this.currentLoadStage.getName()); - } - - public boolean isRunning() { - return this.running.get(); - } - - public Map getBindings() { - return bindings; - } - - public ImportCustomizer getImportCustomizer() { - return importCustomizer; - } - - public CustomGroovyScriptEngine getEngine() { - return engine; - } - - public String getCurrentScript() { - return currentScript; - } - - protected void setCurrentScript(String currentScript) { - this.currentScript = currentScript; - } - - public LoadStage getCurrentLoader() { - return currentLoadStage; - } - - public long getLastCompileTime() { - return compileTime; - } - - public long getLastRunTime() { - return runTime; - } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java index a6fb20165..1d4b87fcd 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java @@ -6,6 +6,8 @@ public enum LoadStage { + MIXIN_EARLY("mixin_early", false, -2000000000), + MIXIN_LATE("mixin_late", false, -1000000000), PRE_INIT("preInit", false, -1000000), INIT("init", false, -1000), POST_INIT("postInit", true, 0); @@ -42,6 +44,10 @@ public int getPriority() { return priority; } + public boolean isMixin() { + return this == MIXIN_EARLY || this == MIXIN_LATE; + } + @Override public String toString() { return name; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java new file mode 100644 index 000000000..5e5c351d2 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -0,0 +1,247 @@ +package com.cleanroommc.groovyscript.sandbox; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.core.LaunchClassLoaderResourceCache; +import com.cleanroommc.groovyscript.sandbox.engine.*; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.ModifyReceiver; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.*; +import groovy.lang.Binding; +import groovy.lang.Script; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.launchwrapper.Launch; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.groovy.control.*; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.Cancellable; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.*; + +public class MixinSandbox extends AbstractGroovySandbox { + + private static MixinSandbox instance; + private static Map injectedResourceCache = new Object2ObjectOpenHashMap<>(); + private static final List earlyMixinClasses = new ArrayList<>(); + private static final List lateMixinClasses = new ArrayList<>(); + + @UnmodifiableView + @ApiStatus.Internal + public static List getEarlyMixinClasses() { + return Collections.unmodifiableList(earlyMixinClasses); + } + + @UnmodifiableView + @ApiStatus.Internal + public static List getLateMixinClasses() { + return Collections.unmodifiableList(lateMixinClasses); + } + + @ApiStatus.Internal + public static void loadEarlyMixins() throws Exception { + if (MixinSandbox.instance != null) { + throw new IllegalStateException("Mixins already loaded"); + } + MixinSandbox.instance = new MixinSandbox(); + MixinSandbox.instance.loadedClasses.clear(); + MixinSandbox.instance.run(LoadStage.MIXIN_EARLY); + + Field lclBytecodesField = Launch.classLoader.getClass().getDeclaredField("resourceCache"); + lclBytecodesField.setAccessible(true); + //noinspection unchecked + Map resourceCache = (Map) lclBytecodesField.get(Launch.classLoader); + lclBytecodesField.set(Launch.classLoader, new LaunchClassLoaderResourceCache(resourceCache, MixinSandbox.injectedResourceCache)); + + // called later than mixin booter, we can now try to compile groovy mixins + // the groovy mixins need to be compiled to bytes manually first + Collection groovyMixins = MixinSandbox.instance.collectCompiledMixins(); + if (groovyMixins.isEmpty()) { + LOG.info("No early groovy mixins configured"); + return; + } + // create and register config + Mixins.addConfiguration("mixin.groovyscript.custom.early.json"); + earlyMixinClasses.addAll(groovyMixins); // mixins are registered by a mixin config plugin at core.MixinPlugin + } + + @ApiStatus.Internal + public static void loadLateMixins() { + SandboxData.onLateInit(); + MixinSandbox.instance.loadedClasses.clear(); + MixinSandbox.instance.run(LoadStage.MIXIN_LATE); + MixinSandbox.instance.getEngine().writeIndex(); + // from now on we forbid modifying the map + MixinSandbox.injectedResourceCache = Collections.unmodifiableMap(MixinSandbox.injectedResourceCache); + + Collection groovyMixins = instance.collectCompiledMixins(); + if (groovyMixins.isEmpty()) { + LOG.info("No late groovy mixins configured"); + return; + } + lateMixinClasses.addAll(groovyMixins); // mixins are registered by a mixin config plugin at core.MixinPlugin + + } + + public static final Logger LOG = LogManager.getLogger("GroovyScript-MixinSandbox"); + public static final boolean DEBUG = true; + + private final Set loadedClasses = new ObjectOpenHashSet<>(); + + private MixinSandbox() {} + + @Override + protected MixinScriptEngine createEngine(CompilerConfiguration config) { + return new MixinScriptEngine(SandboxData.getRootUrls(), SandboxData.getMixinScriptCachePath(), SandboxData.getScriptFile(), config) + .onClassLoaded(cc -> { + if (injectedResourceCache.put(cc.getName(), cc.getData()) == null) { + if (cc.isMixin()) { + GroovyLog.get().info(" - loaded mixin class {}", cc.getName()); + } else { + GroovyLog.get().info(" - loaded class {}", cc.getName()); + } + } + loadedClasses.add(cc); + }); + } + + @Override + protected void initConfig(CompilerConfiguration config) { + getImportCustomizer().addImports( + Arrays.asList( + Mixin.class, + Inject.class, + At.class, + CallbackInfo.class, + CallbackInfoReturnable.class, + Coerce.class, + Constant.class, + Desc.class, + Descriptors.class, + Group.class, + ModifyArg.class, + ModifyArgs.class, + ModifyConstant.class, + ModifyVariable.class, + Redirect.class, + Slice.class, + Surrogate.class, + Final.class, + Mutable.class, + Overwrite.class, + Pseudo.class, + Shadow.class, + Unique.class, + SoftOverride.class, + Implements.class, + Interface.class, + Intrinsic.class, + WrapOperation.class, + ModifyExpressionValue.class, + ModifyReceiver.class, + ModifyReturnValue.class, + Local.class, + Share.class, + LocalRef.class, + LocalIntRef.class, + LocalLongRef.class, + LocalFloatRef.class, + LocalDoubleRef.class, + LocalBooleanRef.class, + LocalByteRef.class, + LocalShortRef.class, + Cancellable.class, + Invoker.class, + Accessor.class, + WrapMethod.class + ) + .stream() + .map(Class::getName) + .toArray(String[]::new)); + super.initConfig(config); + } + + @Override + public boolean canRunInStage(LoadStage stage) { + return stage.isMixin(); + } + + @Override + protected void preRun() { + if (ScriptEngine.DELETE_CACHE_ON_RUN) getEngine().deleteScriptCache(); + GroovyLog.get().infoMC("Loading mixin scripts in loader '{}'", getCurrentLoader()); + } + + @Override + protected void postRun() {} + + @Override + protected void runScript(Script script) throws Throwable { + throw new UnsupportedOperationException("Mixin scripts can not be run!"); + } + + @Override + protected void runClass(Class script) throws Throwable { + throw new UnsupportedOperationException("Mixin scripts can not be run!"); + } + + @Override + protected void loadScripts(Binding binding, Set executedClasses, boolean run) throws Throwable { + if (getCurrentLoader() == LoadStage.MIXIN_EARLY) { + super.loadScripts(binding, executedClasses, run); + return; + } + FileUtil.cleanScriptPathWarnedCache(); + Collection files = getScriptFiles(); + if (files.isEmpty()) return; + List scripts = getEngine().findScripts(files); + if (scripts.isEmpty()) return; + for (CompiledScript compiledScript : scripts) { + if (!executedClasses.contains(compiledScript.getPath()) && compiledScript.requiresModLoaded()) { + loadScript(compiledScript, binding, run); + Class clz = compiledScript.getScriptClass(); + if (!compiledScript.preprocessorCheckFailed() && clz != null && isClassScript(clz)) { + executedClasses.add(compiledScript.getPath()); + } + } + } + } + + private Collection collectCompiledMixins() { + List mixinClasses = new ArrayList<>(); + for (CompiledClass cc : this.loadedClasses) { + if (!cc.isMixin()) continue; + if (!cc.getName().startsWith(SandboxData.MIXIN_PKG + '.')) throw new IllegalArgumentException(); + mixinClasses.add(cc.getName().substring(SandboxData.MIXIN_PKG.length() + 1)); + } + this.loadedClasses.clear(); + return mixinClasses; + } + + @Override + public Collection getScriptFiles() { + return SandboxData.getSortedFilesOf(getScriptRoot(), Collections.singleton(SandboxData.MIXIN_PKG + "/"), false); + } + + @Override + protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) { + long t = System.currentTimeMillis(); + getEngine().loadScript(compiledScript); + this.compileTime += System.currentTimeMillis() - t; + this.loadedClasses.add(compiledScript); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java index 04bcc1f34..ec80ac56b 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/Preprocessor.java @@ -3,50 +3,62 @@ import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.helper.Alias; +import com.cleanroommc.groovyscript.helper.PairList; import com.cleanroommc.groovyscript.packmode.Packmode; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; import com.google.common.base.CaseFormat; import io.sommers.packmode.api.PackModeAPI; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; -import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Loader; +import org.apache.commons.lang3.tuple.Pair; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.function.BiPredicate; public class Preprocessor { private static final Object2ObjectArrayMap> PREPROCESSORS = new Object2ObjectArrayMap<>(); private static final String[] NO_ARGS = new String[0]; + public static final String NO_RUN = "NO_RUN"; + public static final String DEBUG_ONLY = "DEBUG_ONLY"; + public static final String NO_RELOAD = "NO_RELOAD"; + public static final String MODS_LOADED = "MODS_LOADED"; + public static final String SIDE = "SIDE"; + public static final String PACKMODE = "PACKMODE"; + + public static final String SIDE_CLIENT = "CLIENT"; + public static final String SIDE_SERVER = "SERVER"; public static void registerPreprocessor(String name, BiPredicate test) { PREPROCESSORS.put(name.toUpperCase(Locale.ROOT), test); } static { - registerPreprocessor("NO_RUN", (file, args) -> false); - registerPreprocessor("DEBUG_ONLY", (file, args) -> GroovyScript.getRunConfig().isDebug()); - registerPreprocessor("NO_RELOAD", (file, args) -> !ReloadableRegistryManager.isFirstLoad()); - registerPreprocessor("MODS_LOADED", Preprocessor::checkModsLoaded); - registerPreprocessor("SIDE", Preprocessor::checkSide); - registerPreprocessor("PACKMODE", Preprocessor::checkPackmode); + registerPreprocessor(NO_RUN, (file, args) -> false); + registerPreprocessor(DEBUG_ONLY, (file, args) -> GroovyScript.getRunConfig().isDebug()); + registerPreprocessor(NO_RELOAD, (file, args) -> !ReloadableRegistryManager.isFirstLoad()); + registerPreprocessor(MODS_LOADED, Preprocessor::checkModsLoaded); + registerPreprocessor(SIDE, Preprocessor::checkSide); + registerPreprocessor(PACKMODE, Preprocessor::checkPackmode); } - public static List parsePreprocessors(File file) { - List preprocessors = new ArrayList<>(); + public static PairList parsePreprocessors(File file) { + PairList preprocessors = new PairList<>(); parsePreprocessors(file, preprocessors); - return preprocessors.isEmpty() ? Collections.emptyList() : preprocessors; + return preprocessors; } public static int getImportStartLine(File file) { - return parsePreprocessors(file, new ArrayList<>()); + return parsePreprocessors(file, new PairList<>()); } - private static int parsePreprocessors(File file, List preprocessors) { + private static int parsePreprocessors(File file, PairList preprocessors) { int lines = 0; int empty = 0; boolean lastEmpty = false; @@ -84,8 +96,16 @@ private static int parsePreprocessors(File file, List preprocessors) { isComment = false; } - if (isPreprocessor(line)) { - preprocessors.add(line); + String preprocessorName = line; + String args = null; + int i = line.indexOf(':'); + if (i > 0) { + preprocessorName = line.substring(0, i); + args = line.substring(i + 1); + } + preprocessorName = preprocessorName.trim().toUpperCase(Locale.ENGLISH); + if (PREPROCESSORS.containsKey(preprocessorName)) { + preprocessors.add(preprocessorName, args); } } } catch (IOException e) { @@ -94,9 +114,17 @@ private static int parsePreprocessors(File file, List preprocessors) { return preprocessors.isEmpty() ? 0 : lines - empty - 1; } - public static boolean validatePreprocessor(File file, List preprocessors) { - for (String pp : preprocessors) { - if (!processPreprocessor(file, pp)) { + public static boolean containsPreProcessor(String ppName, PairList preprocessors) { + if (preprocessors == null || preprocessors.isEmpty()) return false; + for (String name : preprocessors.getLeftIterable()) { + if (ppName.equals(name)) return true; + } + return false; + } + + public static boolean validatePreprocessor(File file, PairList preprocessors) { + for (Pair pp : preprocessors) { + if (!processPreprocessor(file, pp.getLeft(), pp.getRight())) { return false; } } @@ -108,21 +136,27 @@ private static boolean isPreprocessor(String line) { return PREPROCESSORS.containsKey(s.toUpperCase(Locale.ROOT)); } - private static boolean processPreprocessor(File file, String line) { - String[] parts = line.split(":", 2); + private static boolean processPreprocessor(File file, String name, String rawArgs) { String[] args = NO_ARGS; - if (parts.length > 1) { - args = parts[1].split(","); + if (rawArgs != null && !rawArgs.isEmpty()) { + args = rawArgs.split(","); for (int i = 0; i < args.length; i++) { args[i] = args[i].trim(); } } - String s = parts[0]; - BiPredicate preprocessor = PREPROCESSORS.get(s.toUpperCase(Locale.ROOT)); + BiPredicate preprocessor = PREPROCESSORS.get(name); + if (preprocessor == null) { + throw new NullPointerException("Preprocessor '" + name + "' was previously valid, but was now not found!"); + } return preprocessor.test(file, args); } private static boolean checkModsLoaded(File file, String[] mods) { + if (!SandboxData.isInitialisedLate()) { + // mod data is not yet loaded + // silently fail preprocessor + return false; + } for (String mod : mods) { if (!Loader.isModLoaded(mod)) { return false; @@ -137,11 +171,11 @@ private static boolean checkSide(File file, String[] sides) { return true; } String side = sides[0].toUpperCase(); - if ("CLIENT".equals(side)) { - return FMLCommonHandler.instance().getSide().isClient(); + if (SIDE_CLIENT.equals(side)) { + return SandboxData.getPhysicalSide().isClient(); } - if ("SERVER".equals(side)) { - return FMLCommonHandler.instance().getSide().isServer(); + if (SIDE_SERVER.equals(side)) { + return SandboxData.getPhysicalSide().isServer(); } GroovyLog.get().error("Side processor argument in file '{}' must be CLIENT or SERVER (lower case is allowed too)", file.getName()); return true; @@ -151,8 +185,8 @@ private static boolean checkPackmode(File file, String[] modes) { for (String mode : modes) { if (!Packmode.isValidPackmode(mode)) { List valid = GroovyScript.getRunConfig().isIntegratePackmodeMod() - ? PackModeAPI.getInstance().getPackModes() - : GroovyScript.getRunConfig().getPackmodeList(); + ? PackModeAPI.getInstance().getPackModes() + : GroovyScript.getRunConfig().getPackmodeList(); GroovyLog.get().error("The packmode '{}' specified in file '{}' does not exist. Valid values are {}", mode, file.getName(), valid); } else if (Packmode.getPackmode().equals(Alias.autoConvertTo(mode, CaseFormat.LOWER_UNDERSCORE))) { return true; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java index 532ccd950..e7b749bf3 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java @@ -61,6 +61,9 @@ public static JsonObject createDefaultJson() { modMetadata.version = "0.0.0"; } + public static final String separatorRegex = File.separatorChar == '\\' ? "/" : "\\\\"; + public static final String separatorReplacement = File.separatorChar == '\\' ? "\\\\" : File.separator; + private final String packName; private final String packId; private final String version; @@ -70,11 +73,8 @@ public static JsonObject createDefaultJson() { private final Set packmodeSet = new ObjectOpenHashSet<>(); private final Map> packmodePaths = new Object2ObjectOpenHashMap<>(); private boolean integratePackmodeMod; - // TODO asm - private final String asmClass = null; private boolean debug; - private final boolean invalidPackId; private boolean warnedAboutInvalidPackId; private int packmodeConfigState; @@ -129,9 +129,6 @@ public RunConfig(JsonObject json) { @ApiStatus.Internal public void reload(JsonObject json, boolean init) { - if (GroovyScript.isSandboxLoaded() && GroovyScript.getSandbox().isRunning()) { - throw new RuntimeException(); - } this.debug = JsonHelper.getBoolean(json, false, "debug"); this.loaderPaths.clear(); this.packmodeList.clear(); @@ -139,9 +136,6 @@ public void reload(JsonObject json, boolean init) { this.packmodePaths.clear(); this.packmodeConfigState = 0; - String regex = File.separatorChar == '\\' ? "/" : "\\\\"; - String replacement = getSeparator(); - JsonElement el = json.get("loaders"); JsonObject jsonLoaders; if (el == null || !el.isJsonObject()) { @@ -182,7 +176,7 @@ public void reload(JsonObject json, boolean init) { List paths = new ArrayList<>(); for (JsonElement element : loader) { - String path = sanitizePath(element.getAsString().replaceAll(regex, replacement)); + String path = sanitizePath(element.getAsString()); if (paths.contains(path) || !checkValid(errorMsg, pathsList, entry.getKey(), path)) continue; paths.add(path); } @@ -351,6 +345,7 @@ public Collection getSortedFiles(File root, String loader) { } private static String sanitizePath(String path) { + path = path.replaceAll(separatorRegex, separatorReplacement); while (path.endsWith("/") || path.endsWith("\\")) { path = path.substring(0, path.length() - 1); } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java index 5b107783a..81360079d 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java @@ -1,7 +1,14 @@ package com.cleanroommc.groovyscript.sandbox; import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.helper.JsonHelper; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.thread.SidedThreadGroup; +import net.minecraftforge.fml.relauncher.FMLLaunchHandler; +import net.minecraftforge.fml.relauncher.Side; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.ApiStatus; @@ -11,13 +18,12 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Objects; +import java.nio.file.StandardOpenOption; +import java.util.*; import java.util.stream.Stream; /** @@ -25,6 +31,8 @@ */ public class SandboxData { + public static final String MIXIN_PKG = "mixins"; + private static final FileVisitOption[] FOLLOW_LINKS = { FileVisitOption.FOLLOW_LINKS }; @@ -33,20 +41,28 @@ public class SandboxData { public static final String[] GROOVY_SUFFIXES = { ".groovy", ".gvy", ".gy", ".gsh" }; + private static ActualSide physicalSide; + private static final ThreadLocal logicalSide = new ThreadLocal<>(); private static File minecraftHome; private static File scriptPath; + private static File mixinPath; private static File runConfigFile; private static File resourcesFile; - private static File cachePath; - private static URL rootUrl; + private static File cacheBasePath; + private static File standardScriptCachePath; + private static File mixinScriptCachePath; private static URL[] rootUrls; + private static URL[] mixinRootUrls; + private static RunConfig runConfig; private static boolean initialised = false; + private static boolean initialisedLate = false; private SandboxData() {} @ApiStatus.Internal public static void initialize(File minecraftHome, Logger log) { if (SandboxData.initialised) return; + physicalSide = ActualSide.ofPhysicalSide(FMLLaunchHandler.side()); try { SandboxData.minecraftHome = Objects.requireNonNull(minecraftHome, "Minecraft Home can't be null!").getCanonicalFile(); @@ -54,7 +70,9 @@ public static void initialize(File minecraftHome, Logger log) { GroovyLog.get().errorMC("Failed to canonicalize minecraft home path '" + minecraftHome + "'!"); throw new RuntimeException(e); } - cachePath = new File(SandboxData.minecraftHome, "cache" + File.separator + "groovy"); + cacheBasePath = new File(SandboxData.minecraftHome, "cache" + File.separator + "groovy"); + standardScriptCachePath = new File(cacheBasePath, "standard"); + mixinScriptCachePath = new File(cacheBasePath, "mixin"); // If we are launching with the environment variable set to use the examples folder, use the examples folder for easy and consistent testing. if (Boolean.parseBoolean(System.getProperty("groovyscript.use_examples_folder"))) { scriptPath = new File(SandboxData.minecraftHome.getParentFile(), "examples"); @@ -67,17 +85,52 @@ public static void initialize(File minecraftHome, Logger log) { log.error("Failed to canonicalize groovy script path '{}'!", scriptPath); log.throwing(e); } + mixinPath = new File(scriptPath, MIXIN_PKG); runConfigFile = new File(scriptPath, "runConfig.json"); resourcesFile = new File(scriptPath, "assets"); try { - rootUrl = scriptPath.toURI().toURL(); rootUrls = new URL[]{ - rootUrl + scriptPath.toURI().toURL() + }; + mixinRootUrls = new URL[]{ + mixinPath.toURI().toURL() }; } catch (MalformedURLException e) { throw new IllegalStateException("Failed to create URL from script path " + scriptPath); } initialised = true; + reloadRunConfig(true); + } + + @ApiStatus.Internal + public static void onLateInit() { + if (initialisedLate) return; + initialisedLate = true; + physicalSide = ActualSide.ofPhysicalSide(FMLCommonHandler.instance().getSide()); + } + + public static @NotNull ActualSide getPhysicalSide() { + ensureLoaded(); + return physicalSide; + } + + public static ActualSide getLogicalSide() { + ensureLoaded(); + if (!SandboxData.initialisedLate) return SandboxData.physicalSide; + ActualSide side = logicalSide.get(); + if (side == null) { + // try to obtain the effective side similar to FMLCommonHandler + ThreadGroup group = Thread.currentThread().getThreadGroup(); + if (group instanceof SidedThreadGroup sidedThreadGroup) { + // current thread is valid -> store it in thread local + side = ActualSide.ofLogicalSide(sidedThreadGroup.getSide()); + logicalSide.set(side); + return side; + } + // current thread is invalid to retrieve side -> fallback to physical side + return SandboxData.physicalSide; + } + return side; } public static @NotNull String getScriptPath() { @@ -94,6 +147,11 @@ public static void initialize(File minecraftHome, Logger log) { return scriptPath; } + public static @NotNull File getMixinFile() { + ensureLoaded(); + return mixinPath; + } + public static @NotNull File getResourcesFile() { ensureLoaded(); return resourcesFile; @@ -104,14 +162,23 @@ public static void initialize(File minecraftHome, Logger log) { return runConfigFile; } - public static @NotNull File getCachePath() { + public static @NotNull File getCacheBasePath() { ensureLoaded(); - return cachePath; + return cacheBasePath; } - public static @NotNull URL getRootUrl() { + public static @NotNull File getStandardScriptCachePath() { ensureLoaded(); - return rootUrl; + return standardScriptCachePath; + } + + public static @NotNull File getMixinScriptCachePath() { + ensureLoaded(); + return mixinScriptCachePath; + } + + public static @NotNull URL getRootUrl() { + return getRootUrls()[0]; } public static @NotNull URL[] getRootUrls() { @@ -119,6 +186,20 @@ public static void initialize(File minecraftHome, Logger log) { return rootUrls; } + public static @NotNull URL getMixinRootUrl() { + return getMixinRootUrls()[0]; + } + + public static @NotNull URL[] getMixinRootUrls() { + ensureLoaded(); + return mixinRootUrls; + } + + public static RunConfig getRunConfig() { + ensureLoaded(); + return runConfig; + } + private static void ensureLoaded() { if (!initialised) { throw new IllegalStateException("Sandbox data is not yet Initialised."); @@ -129,6 +210,10 @@ public static boolean isInitialised() { return initialised; } + public static boolean isInitialisedLate() { + return initialisedLate; + } + public static File getRelativeFile(File file) { return new File(getRelativePath(file.getPath())); } @@ -137,6 +222,36 @@ public static String getRelativePath(String path) { return FileUtil.relativize(getScriptPath(), path); } + @ApiStatus.Internal + public static void reloadRunConfig(boolean init) { + JsonElement element = JsonHelper.loadJson(getRunConfigFile()); + if (element == null || !element.isJsonObject()) element = new JsonObject(); + JsonObject json = element.getAsJsonObject(); + if (runConfig == null) { + if (!Files.exists(getRunConfigFile().toPath())) { + json = RunConfig.createDefaultJson(); + runConfig = createRunConfig(json); + } else { + runConfig = new RunConfig(json); + } + } + runConfig.reload(json, init); + } + + private static RunConfig createRunConfig(JsonObject json) { + JsonHelper.saveJson(getRunConfigFile(), json); + File main = new File(getScriptFile().getPath() + File.separator + "postInit" + File.separator + "main.groovy"); + if (!Files.exists(main.toPath())) { + try { + main.getParentFile().mkdirs(); + Files.write(main.toPath(), "\nlog.info('Hello World!')\n".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new RunConfig(json); + } + static Collection getSortedFilesOf(File root, Collection paths, boolean debug) { Object2IntLinkedOpenHashMap files = new Object2IntLinkedOpenHashMap<>(); String separator = File.separatorChar == '\\' ? "\\\\" : File.separator; @@ -168,6 +283,7 @@ static Collection getSortedFilesOf(File root, Collection paths, bo throw new RuntimeException(e); } } + if (files.isEmpty()) return Collections.emptyList(); return new ArrayList<>(files.keySet()); } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java similarity index 60% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java index 42f3c02a3..5bd91b0f1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java @@ -1,17 +1,20 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; -import com.cleanroommc.groovyscript.api.GroovyLog; -import groovy.lang.GroovyClassLoader; +import com.cleanroommc.groovyscript.sandbox.FileUtil; +import com.cleanroommc.groovyscript.sandbox.SandboxData; import org.apache.commons.lang3.builder.ToStringBuilder; import org.codehaus.groovy.runtime.InvokerHelper; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.util.Map; -class CompiledClass { +@ApiStatus.Internal +public class CompiledClass { public static final String CLASS_SUFFIX = ".clz"; @@ -19,26 +22,26 @@ class CompiledClass { final String name; byte[] data; Class clazz; + boolean mixin; + boolean earlyMixin; public CompiledClass(String path, String name) { this.path = path; this.name = name; } - public void onCompile(byte[] data, Class clazz, String basePath) { - this.data = data; - onCompile(clazz, basePath); + public CompiledClass(String path, String name, boolean mixin) { + this(path, name); + this.mixin = mixin; } - public void onCompile(Class clazz, String basePath) { + public void onCompile(byte @NotNull [] data, @Nullable Class clazz, String basePath) { + this.data = data; this.clazz = clazz; - if (!this.name.equals(clazz.getName())) throw new IllegalArgumentException(); - //this.name = clazz.getName(); - if (this.data == null) { - GroovyLog.get().errorMC("The class doesnt seem to be compiled yet. (" + name + ")"); - return; + if (clazz != null && !this.name.equals(clazz.getName())) { + throw new IllegalArgumentException("Expected class name to be " + this.name + ", but was " + clazz.getName()); } - if (!CustomGroovyScriptEngine.ENABLE_CACHE) return; + if (!ScriptEngine.ENABLE_CACHE) return; try { File file = getDataFile(basePath); file.getParentFile().mkdirs(); @@ -51,15 +54,9 @@ public void onCompile(Class clazz, String basePath) { } } - protected void ensureLoaded(GroovyClassLoader classLoader, Map cache, String basePath) { - if (this.clazz == null) { - this.clazz = classLoader.defineClass(this.name, this.data); - cache.put(this.name, this); - } - } - public boolean readData(String basePath) { - if (this.data != null && CustomGroovyScriptEngine.ENABLE_CACHE) return true; + if (!ScriptEngine.ENABLE_CACHE) return false; + if (this.data != null) return true; File file = getDataFile(basePath); if (!file.exists()) return false; try { @@ -76,11 +73,15 @@ public void deleteCache(String cachePath) { } catch (IOException e) { throw new RuntimeException(e); } + removeClass(); + this.data = null; + } + + protected void removeClass() { if (this.clazz != null) { InvokerHelper.removeClass(this.clazz); this.clazz = null; } - this.data = null; } protected File getDataFile(String basePath) { @@ -95,6 +96,26 @@ public String getPath() { return path; } + public Class getScriptClass() { + return clazz; + } + + public boolean isMixin() { + return mixin; + } + + public boolean hasData() { + return data != null; + } + + public byte[] getData() { + return data; + } + + public boolean hasClass() { + return isMixin() || this.clazz != null; + } + @Override public String toString() { return new ToStringBuilder(this).append("name", name).toString(); diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java similarity index 55% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java index 36defa117..c8788a43b 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java @@ -1,20 +1,24 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; -import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.helper.JsonHelper; +import com.cleanroommc.groovyscript.helper.PairList; +import com.cleanroommc.groovyscript.sandbox.Preprocessor; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import groovy.lang.GroovyClassLoader; import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Map; -class CompiledScript extends CompiledClass { +@ApiStatus.Internal +public class CompiledScript extends CompiledClass { public static String classNameFromPath(String path) { int i = path.lastIndexOf('.'); @@ -24,7 +28,8 @@ public static String classNameFromPath(String path) { final List innerClasses = new ArrayList<>(); long lastEdited; - List preprocessors; + PairList preprocessors; + boolean requiresModLoaded = false; private boolean preprocessorCheckFailed; private boolean requiresReload; @@ -42,9 +47,9 @@ public boolean isClosure() { } @Override - public void onCompile(Class clazz, String basePath) { + public void onCompile(byte @NotNull [] data, @Nullable Class clazz, String basePath) { + super.onCompile(data, clazz, basePath); setRequiresReload(this.data == null); - super.onCompile(clazz, basePath); } public CompiledClass findInnerClass(String clazz) { @@ -58,35 +63,33 @@ public CompiledClass findInnerClass(String clazz) { return comp; } - public void ensureLoaded(GroovyClassLoader classLoader, Map cache, String basePath) { - for (CompiledClass comp : this.innerClasses) { - if (comp.clazz == null) { - if (comp.readData(basePath)) { - comp.ensureLoaded(classLoader, cache, basePath); - } else { - GroovyLog.get().error("Error loading inner class {} for class {}", comp.name, this.name); - } - } - } - super.ensureLoaded(classLoader, cache, basePath); - } - public @NotNull JsonObject toJson() { JsonObject jsonEntry = new JsonObject(); jsonEntry.addProperty("name", this.name); jsonEntry.addProperty("path", this.path); jsonEntry.addProperty("lm", this.lastEdited); + jsonEntry.addProperty("mixin", this.mixin); if (!this.innerClasses.isEmpty()) { JsonArray inner = new JsonArray(); for (CompiledClass comp : this.innerClasses) { - inner.add(comp.name); + JsonObject json = new JsonObject(); + json.addProperty("name", comp.name); + json.addProperty("mixin", comp.mixin); + inner.add(json); } jsonEntry.add("inner", inner); } if (this.preprocessors != null && !this.preprocessors.isEmpty()) { JsonArray jsonPp = new JsonArray(); - for (String pp : this.preprocessors) { - jsonPp.add(pp); + for (Pair pp : this.preprocessors) { + if (pp.getRight() == null) { + jsonPp.add(pp.getLeft()); + } else { + JsonObject json = new JsonObject(); + json.addProperty("name", pp.getLeft()); + json.addProperty("args", pp.getRight()); + jsonPp.add(json); + } } jsonEntry.add("preprocessors", jsonPp); } @@ -101,13 +104,21 @@ public static CompiledScript fromJson(JsonObject json, String scriptRoot, String if (new File(scriptRoot, cs.path).exists()) { if (json.has("inner")) { for (JsonElement element : json.getAsJsonArray("inner")) { - cs.innerClasses.add(new CompiledClass(cs.path, element.getAsString())); + JsonObject o = element.getAsJsonObject(); + cs.innerClasses.add(new CompiledClass(cs.path, o.get("name").getAsString(), o.get("mixin").getAsBoolean())); } } if (json.has("preprocessors")) { - cs.preprocessors = new ArrayList<>(); + cs.requiresModLoaded = false; + cs.preprocessors = new PairList<>(); for (JsonElement element : json.getAsJsonArray("preprocessors")) { - cs.preprocessors.add(element.getAsString()); + if (element.isJsonPrimitive()) { + cs.preprocessors.add(element.getAsString(), null); + } else if (element.isJsonObject()) { + String name = element.getAsJsonObject().get("name").getAsString(); + cs.preprocessors.add(name, element.getAsJsonObject().get("args").getAsString()); + cs.requiresModLoaded |= Preprocessor.MODS_LOADED.equals(name); + } } } return cs; @@ -125,11 +136,50 @@ public void deleteCache(String cachePath) { } } + @Override + protected void removeClass() { + super.removeClass(); + for (CompiledClass cc : this.innerClasses) { + cc.removeClass(); + } + } + + public boolean checkRequiresReload(File file, long lastModified, String rootPath) { + // the file needs to be reparsed if: + // - caching is disabled + // - it wasn't parsed before + // - there is no class (mixins don't have classes) + // - the file was modified since the last parsing + setRequiresReload(!ScriptEngine.ENABLE_CACHE || !readData(rootPath) || isMissingAnyClass() || lastModified > this.lastEdited); + if (requiresReload()) { + removeClass(); + // parse preprocessors if file was modified + if (this.preprocessors == null || lastModified > this.lastEdited) { + this.preprocessors = Preprocessor.parsePreprocessors(file); + this.requiresModLoaded = Preprocessor.containsPreProcessor(Preprocessor.MODS_LOADED, this.preprocessors); + } + this.lastEdited = lastModified; + } + return requiresReload(); + } + + public boolean isMissingAnyClass() { + if (!hasClass()) return true; + for (CompiledClass cc : this.innerClasses) { + if (!cc.hasClass()) return true; + } + return false; + } + public boolean checkPreprocessorsFailed(File basePath) { setPreprocessorCheckFailed(this.preprocessors != null && !this.preprocessors.isEmpty() && !Preprocessor.validatePreprocessor(new File(basePath, this.path), this.preprocessors)); return preprocessorCheckFailed(); } + public List getInnerClasses() { + return Collections.unmodifiableList(this.innerClasses); + } + public boolean requiresReload() { return this.requiresReload; } @@ -146,6 +196,10 @@ protected void setPreprocessorCheckFailed(boolean preprocessorCheckFailed) { this.preprocessorCheckFailed = preprocessorCheckFailed; } + public boolean requiresModLoaded() { + return requiresModLoaded; + } + @Override public String toString() { return new ToStringBuilder(this).append("name", name) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java similarity index 92% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java index afa59e155..ee7d6e723 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java @@ -1,9 +1,8 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyCodeSource; import groovy.lang.GroovyResourceLoader; -import groovy.util.CharsetToolkit; import groovyjarjarasm.asm.ClassVisitor; import groovyjarjarasm.asm.ClassWriter; import net.minecraft.launchwrapper.Launch; @@ -17,12 +16,12 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.CodeSource; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.Map; -import java.util.function.BiConsumer; public abstract class GroovyScriptClassLoader extends GroovyClassLoader { @@ -50,9 +49,7 @@ protected void init() { private String initSourceEncoding(CompilerConfiguration config) { String sourceEncoding = config.getSourceEncoding(); if (null == sourceEncoding) { - // Keep the same default source encoding with the one used by #parseClass(InputStream, String) - // TODO should we use org.codehaus.groovy.control.CompilerConfiguration.DEFAULT_SOURCE_ENCODING instead? - return CharsetToolkit.getDefaultSystemCharset().name(); + return StandardCharsets.UTF_8.displayName(); } return sourceEncoding; } @@ -286,12 +283,11 @@ protected GroovyClassLoader.ClassCollector createCollector(CompilationUnit unit, public static class ClassCollector implements CompilationUnit.ClassgenCallback { - private Class generatedClass; - private final GroovyScriptClassLoader cl; - private final SourceUnit su; - private final CompilationUnit unit; - private final Collection> loadedClasses; - private BiConsumer> creatClassCallback; + protected Class generatedClass; + protected final GroovyScriptClassLoader cl; + protected final SourceUnit su; + protected final CompilationUnit unit; + protected final Collection> loadedClasses; protected ClassCollector(GroovyScriptClassLoader cl, CompilationUnit unit, SourceUnit su) { this.cl = cl; @@ -304,14 +300,23 @@ public GroovyScriptClassLoader getDefiningClassLoader() { return cl; } - protected Class createClass(byte[] code, ClassNode classNode) { + @Override + public void call(ClassVisitor classWriter, ClassNode classNode) { + byte[] code = ((ClassWriter) classWriter).toByteArray(); + Class clz = generateClass(postProcessBytecode(code, classNode), classNode); + } + + public byte[] postProcessBytecode(byte[] bytecode, ClassNode classNode) { BytecodeProcessor bytecodePostprocessor = unit.getConfiguration().getBytecodePostprocessor(); - byte[] fcode = code; if (bytecodePostprocessor != null) { - fcode = bytecodePostprocessor.processBytecode(classNode.getName(), fcode); + bytecode = bytecodePostprocessor.processBytecode(classNode.getName(), bytecode); } + return bytecode; + } + + protected Class generateClass(byte[] code, ClassNode classNode) { GroovyScriptClassLoader cl = getDefiningClassLoader(); - Class theClass = cl.defineClass(classNode.getName(), fcode, 0, fcode.length, unit.getAST().getCodeSource()); + Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource()); this.loadedClasses.add(theClass); if (generatedClass == null) { @@ -322,31 +327,12 @@ protected Class createClass(byte[] code, ClassNode classNode) { if (mn != null) main = mn.getClasses().get(0); if (msu == su && main == classNode) generatedClass = theClass; } - - if (this.creatClassCallback != null) { - this.creatClassCallback.accept(code, theClass); - } return theClass; } - protected Class onClassNode(ClassWriter classWriter, ClassNode classNode) { - byte[] code = classWriter.toByteArray(); - return createClass(code, classNode); - } - - @Override - public void call(ClassVisitor classWriter, ClassNode classNode) { - onClassNode((ClassWriter) classWriter, classNode); - } - public Collection> getLoadedClasses() { return this.loadedClasses; } - - public ClassCollector creatClassCallback(BiConsumer> creatClassCallback) { - this.creatClassCallback = creatClassCallback; - return this; - } } public static class InnerLoader extends GroovyScriptClassLoader { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java new file mode 100644 index 000000000..00c4bde6a --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java @@ -0,0 +1,91 @@ +package com.cleanroommc.groovyscript.sandbox.engine; + +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.SourceUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; + +import java.io.File; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; + +@ApiStatus.Internal +public class MixinScriptEngine extends ScriptEngine { + + private static final ClassNode MIXIN_NODE = ClassHelper.makeCached(Mixin.class); + + private Consumer onClassLoaded; + + public MixinScriptEngine(URL[] scriptEnvironment, File cacheRoot, File scriptRoot, + CompilerConfiguration config) { + super(scriptEnvironment, cacheRoot, scriptRoot, config); + } + + @Override + protected ScriptClassLoader createClassLoader() { + return new MixinScriptClassLoader(MixinScriptEngine.class.getClassLoader(), getConfig(), Collections.unmodifiableMap(getLoadedClasses())); + } + + public MixinScriptEngine onClassLoaded(Consumer onClassLoaded) { + this.onClassLoaded = onClassLoaded; + return this; + } + + @Override + protected void onClassLoaded(CompiledClass cc) { + super.onClassLoaded(cc); + if (this.onClassLoaded != null) { + this.onClassLoaded.accept(cc); + } + } + + @Override + protected @NotNull CompiledClass findCompiledClass(SourceUnit su, ClassNode classNode, byte[] bytecode) { + CompiledClass cc = super.findCompiledClass(su, classNode, bytecode); + cc.mixin = isMixinClass(classNode); + return cc; + } + + public static boolean isMixinClass(ClassNode classNode) { + for (AnnotationNode annotationNode : classNode.getAnnotations()) { + if (MIXIN_NODE.equals(annotationNode.getClassNode())) { + return true; + } + } + return false; + } + + public class MixinClassCollector extends GroovyScriptClassLoader.ClassCollector { + + protected MixinClassCollector(GroovyScriptClassLoader cl, CompilationUnit unit, SourceUnit su) { + super(cl, unit, su); + } + + @Override + protected Class generateClass(byte[] code, ClassNode classNode) { + CompiledClass cc = findCompiledClass(this.su, classNode, code); + //Class clz = super.generateClass(code, classNode); + onClassCompiled(cc, classNode, code, null); + return null; + } + } + + protected class MixinScriptClassLoader extends ScriptEngine.ScriptClassLoader { + + public MixinScriptClassLoader(ClassLoader loader, CompilerConfiguration config, Map cache) { + super(loader, config, cache); + } + + @Override + protected MixinClassCollector createCustomCollector(CompilationUnit unit, SourceUnit su) { + return new MixinClassCollector(new InnerLoader(this), unit, su); + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java similarity index 75% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java index e5a6951fb..bcbdb7029 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java @@ -1,14 +1,14 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.helper.JsonHelper; +import com.cleanroommc.groovyscript.sandbox.*; import com.google.common.collect.AbstractIterator; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import groovy.lang.GroovyCodeSource; -import groovy.util.ResourceConnector; import groovy.util.ResourceException; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.apache.commons.io.FileUtils; @@ -17,11 +17,11 @@ import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.*; import org.codehaus.groovy.runtime.IOGroovyMethods; -import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.tools.gse.DependencyTracker; import org.codehaus.groovy.tools.gse.StringSetMap; import org.codehaus.groovy.vmplugin.VMPlugin; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -36,13 +36,14 @@ import java.security.CodeSource; import java.util.*; -public class CustomGroovyScriptEngine implements ResourceConnector { +@ApiStatus.Internal +public class ScriptEngine { /** * Changing this number will force the cache to be deleted and every script has to be recompiled. * Useful when changes to the compilation process were made. */ - public static final int CACHE_VERSION = 4; + public static final int CACHE_VERSION = 5; /** * Setting this to false will cause compiled classes to never be cached. * As a side effect some compilation behaviour might change. Can be useful for debugging. @@ -72,15 +73,19 @@ private static synchronized ThreadLocal getLocalData() { private final Map index = new Object2ObjectOpenHashMap<>(); private final Map loadedClasses = new Object2ObjectOpenHashMap<>(); - public CustomGroovyScriptEngine(URL[] scriptEnvironment, File cacheRoot, File scriptRoot, CompilerConfiguration config) { + public ScriptEngine(URL[] scriptEnvironment, File cacheRoot, File scriptRoot, CompilerConfiguration config) { this.scriptEnvironment = scriptEnvironment; this.cacheRoot = cacheRoot; this.scriptRoot = scriptRoot; this.config = config; - this.classLoader = new ScriptClassLoader(CustomGroovyScriptEngine.class.getClassLoader(), config, Collections.unmodifiableMap(this.loadedClasses)); + this.classLoader = createClassLoader(); readIndex(); } + protected ScriptClassLoader createClassLoader() { + return new ScriptClassLoader(ScriptEngine.class.getClassLoader(), config, Collections.unmodifiableMap(this.loadedClasses)); + } + public File getScriptRoot() { return scriptRoot; } @@ -97,26 +102,48 @@ public GroovyScriptClassLoader getClassLoader() { return classLoader; } - public Iterable> getAllLoadedScriptClasses() { - return () -> new AbstractIterator<>() { + protected Map getLoadedClasses() { + return loadedClasses; + } + + public Iterator classIterator() { + return new AbstractIterator<>() { private final Iterator it = loadedClasses.values().iterator(); private Iterator innerClassesIt; @Override - protected Class computeNext() { + protected CompiledClass computeNext() { if (innerClassesIt != null && innerClassesIt.hasNext()) { - return innerClassesIt.next().clazz; + return innerClassesIt.next(); } innerClassesIt = null; CompiledClass cc; while (it.hasNext()) { cc = it.next(); - if (cc instanceof CompiledScript cs && !cs.preprocessorCheckFailed() && cs.clazz != null) { + if (cc instanceof CompiledScript cs && !cs.preprocessorCheckFailed()) { if (!cs.innerClasses.isEmpty()) { innerClassesIt = cs.innerClasses.iterator(); } - return cs.clazz; + return cs; + } + } + return endOfData(); + } + }; + } + + public Iterable> getAllLoadedScriptClasses() { + return () -> new AbstractIterator<>() { + + private final Iterator it = classIterator(); + + @Override + protected Class computeNext() { + while (it.hasNext()) { + CompiledClass cc = it.next(); + if (cc.clazz != null) { + return cc.clazz; } } return endOfData(); @@ -150,7 +177,8 @@ void readIndex() { } } - void writeIndex() { + @ApiStatus.Internal + public void writeIndex() { if (!ENABLE_CACHE) return; JsonObject json = new JsonObject(); json.addProperty("!DANGER!", "DO NOT EDIT THIS FILE!!!"); @@ -166,12 +194,12 @@ void writeIndex() { @ApiStatus.Internal public boolean deleteScriptCache() { - this.index.values().forEach(script -> script.deleteCache(this.cacheRoot.getPath())); this.index.clear(); this.loadedClasses.clear(); getClassLoader().clearCache(); try { - if (this.cacheRoot.exists()) FileUtils.cleanDirectory(this.cacheRoot); + File cache = SandboxData.getCacheBasePath(); + if (cache.exists()) FileUtils.cleanDirectory(cache); return true; } catch (IOException e) { GroovyScript.LOGGER.throwing(e); @@ -179,7 +207,37 @@ public boolean deleteScriptCache() { } } - List findScripts(Collection files) { + private void ensureScriptLoaded(CompiledScript cs) { + for (CompiledClass cc : cs.innerClasses) { + if (cc.clazz == null) { + if (cc.readData(getCacheRoot().getPath())) { + ensureClassLoaded(cc); + } else { + GroovyLog.get().error("Error loading inner class {} for class {}", cc.name, cs.name); + } + } + } + ensureClassLoaded(cs); + } + + private void ensureClassLoaded(CompiledClass cc) { + if (cc.clazz == null) { + if (!cc.isMixin() && cc.data != null) { + cc.clazz = classLoader.defineClass(cc.name, cc.data); + } + this.loadedClasses.put(cc.name, cc); + onClassLoaded(cc); + } + } + + protected void onClassLoaded(CompiledClass cc) {} + + public Collection getCompiledScriptsFromIndex() { + return Collections.unmodifiableCollection(this.index.values()); + } + + @ApiStatus.Internal + public List findScripts(Collection files) { List scripts = new ArrayList<>(files.size()); for (File file : files) { CompiledScript cs = checkScriptLoadability(file); @@ -188,11 +246,12 @@ List findScripts(Collection files) { return scripts; } - void loadScript(CompiledScript script) { - if (script != null && script.requiresReload() && !script.preprocessorCheckFailed()) { + @ApiStatus.Internal + public void loadScript(CompiledScript script) { + if (script.requiresReload() && !script.preprocessorCheckFailed()) { Class clazz = loadScriptClassInternal(new File(script.path), true); script.setRequiresReload(false); - if (script.clazz == null) { + if (script.clazz == null && !script.isMixin()) { // should not happen GroovyLog.get().errorMC("Class for {} was loaded, but didn't receive class created callback!", script.path); if (ENABLE_CACHE) script.clazz = clazz; @@ -214,45 +273,17 @@ CompiledScript checkScriptLoadability(File file) { } // File relativeFile = new File(relativeFileName); long lastModified = file.lastModified(); - CompiledScript comp = this.index.get(relativeFileName); + CompiledScript comp = this.index.computeIfAbsent(relativeFileName, key -> new CompiledScript(key, 0)); - if (ENABLE_CACHE && comp != null && lastModified <= comp.lastEdited && comp.clazz == null && comp.readData(this.cacheRoot.getPath())) { - // class is not loaded, but the cached class bytes are still valid - comp.setRequiresReload(false); - if (comp.checkPreprocessorsFailed(this.scriptRoot)) { - return comp; - } - comp.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); - } else if (!ENABLE_CACHE || (comp == null || comp.clazz == null || lastModified > comp.lastEdited)) { - // class is not loaded and class bytes don't exist yet or script has been edited - if (comp == null) { - comp = new CompiledScript(relativeFileName, 0); - this.index.put(relativeFileName, comp); - } - if (comp.clazz != null) { - InvokerHelper.removeClass(comp.clazz); - comp.clazz = null; - } - comp.setRequiresReload(true); - if (lastModified > comp.lastEdited || comp.preprocessors == null) { - // recompile preprocessors if there is no data or script was edited - comp.preprocessors = Preprocessor.parsePreprocessors(file); - } - comp.lastEdited = lastModified; - if (comp.checkPreprocessorsFailed(this.scriptRoot)) { - // delete class bytes to make sure it's recompiled once the preprocessors returns true - comp.deleteCache(this.cacheRoot.getPath()); - return comp; - } - } else { - // class is loaded and script wasn't edited - comp.setRequiresReload(false); - if (comp.checkPreprocessorsFailed(this.scriptRoot)) { - return comp; - } - comp.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); + boolean requiresReload = comp.checkRequiresReload(file, lastModified, this.cacheRoot.getPath()); + if (comp.checkPreprocessorsFailed(this.scriptRoot)) { + // delete class bytes to make sure it's recompiled once the preprocessors returns true + comp.deleteCache(this.cacheRoot.getPath()); + return comp; + } + if (!requiresReload) { + ensureScriptLoaded(comp); } - comp.setPreprocessorCheckFailed(false); return comp; } @@ -315,26 +346,28 @@ private File findScriptFile(String scriptName) { return clazz; } - /** - * Called via mixin when groovy compiled a class from scripts. - */ - @ApiStatus.Internal - public void onCompileClass(SourceUnit su, String path, Class clazz, byte[] code, boolean inner) { + protected @NotNull CompiledClass findCompiledClass(SourceUnit su, ClassNode classNode, byte[] bytecode) { + // class is null for mixins + String className = classNode.getName(); + String path = su.getName(); String shortPath = FileUtil.relativize(this.scriptRoot.getPath(), path); + String mainClassName = mainClassName(className); // if the script was compiled because another script depends on it, the source unit is wrong // we need to find the source unit of the compiled class - SourceUnit trueSource = su.getAST().getUnit().getScriptSourceLocation(mainClassName(clazz.getName())); + SourceUnit trueSource = su.getAST().getUnit().getScriptSourceLocation(mainClassName); String truePath = trueSource == null ? shortPath : FileUtil.relativize(this.scriptRoot.getPath(), trueSource.getName()); - if (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(clazz.getName())) { - inner = true; - } + boolean inner = className.length() != mainClassName.length() || (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(className)); - boolean finalInner = inner; - CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, finalInner ? -1 : 0)); + CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, inner ? -1 : 0)); CompiledClass innerClass = comp; - if (inner) innerClass = comp.findInnerClass(clazz.getName()); - innerClass.onCompile(code, clazz, this.cacheRoot.getPath()); - this.loadedClasses.put(innerClass.name, innerClass); + if (inner) innerClass = comp.findInnerClass(className); + return innerClass; + } + + protected void onClassCompiled(CompiledClass cc, ClassNode classNode, byte[] bytecode, Class clz) { + cc.onCompile(bytecode, clz, this.cacheRoot.getPath()); + this.loadedClasses.put(cc.name, cc); + onClassLoaded(cc); } /** @@ -349,20 +382,22 @@ public Class onRecompileClass(URL source, String className) { Class c = null; if (cs != null) { if (cs.clazz == null && cs.readData(this.cacheRoot.getPath())) { - cs.ensureLoaded(getClassLoader(), this.loadedClasses, this.cacheRoot.getPath()); + ensureScriptLoaded(cs); } c = cs.clazz; } return c; } - private static String mainClassName(String name) { - return name.contains("$") ? name.split("\\$", 2)[0] : name; + private static String mainClassName(String className) { + int i = className.lastIndexOf('.'); + if (i < 0) i = 0; + i = className.indexOf('$', i + 1); + if (i < 0) return className; + return className.substring(0, i); } - - @Override - public URLConnection getResourceConnection(String resourceName) throws ResourceException { + private URLConnection getResourceConnection(String resourceName) throws ResourceException { // Get the URLConnection URLConnection groovyScriptConn = null; @@ -401,28 +436,11 @@ public URLConnection getResourceConnection(String resourceName) throws ResourceE private static URLConnection openConnection(URL scriptURL) throws IOException { URLConnection urlConnection = scriptURL.openConnection(); - verifyInputStream(urlConnection); - - return scriptURL.openConnection(); - } - - private static void forceClose(URLConnection urlConnection) { - if (urlConnection != null) { - // We need to get the input stream and close it to force the open - // file descriptor to be released. Otherwise, we will reach the limit - // for number of files open at one time. - - try { - verifyInputStream(urlConnection); - } catch (Exception e) { - // Do nothing: We were not going to use it anyway. - } - } - } - - private static void verifyInputStream(URLConnection urlConnection) throws IOException { + // verify connection try (InputStream in = urlConnection.getInputStream()) { + ; } + return scriptURL.openConnection(); } private static class LocalData { @@ -432,7 +450,22 @@ private static class LocalData { final Map precompiledEntries = new HashMap<>(); } - private class ScriptClassLoader extends GroovyScriptClassLoader { + public class ClassCollector extends GroovyScriptClassLoader.ClassCollector { + + protected ClassCollector(GroovyScriptClassLoader cl, CompilationUnit unit, SourceUnit su) { + super(cl, unit, su); + } + + @Override + protected Class generateClass(byte[] code, ClassNode classNode) { + CompiledClass cc = findCompiledClass(this.su, classNode, code); + Class clz = super.generateClass(code, classNode); + onClassCompiled(cc, classNode, code, clz); + return clz; + } + } + + protected class ScriptClassLoader extends GroovyScriptClassLoader { public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config, Map cache) { super(loader, config, cache); @@ -441,7 +474,7 @@ public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config, Map { - onCompileClass(su, su.getName(), clz, code, clz.getName().contains("$")); - }); + return new ScriptEngine.ClassCollector(new InnerLoader(this), unit, su); } @Override @@ -468,7 +499,7 @@ protected CompilationUnit createCompilationUnit(CompilerConfiguration configurat for (String depSourcePath : cache.get(".")) { try { cache.get(depSourcePath); - cu.addSource(getResourceConnection(depSourcePath).getURL()); // todo remove usage of resource connection + cu.addSource(getResourceConnection(depSourcePath).getURL()); } catch (ResourceException e) { /* ignore */ } @@ -490,10 +521,13 @@ protected CompilationUnit createCompilationUnit(CompilerConfiguration configurat @Override public LookupResult findClassNode(String origName, CompilationUnit compilationUnit) { String name = origName.replace('.', '/'); - File scriptFile = CustomGroovyScriptEngine.this.findScriptFileOfClass(name); + File scriptFile = ScriptEngine.this.findScriptFileOfClass(name); if (scriptFile != null) { CompiledScript result = checkScriptLoadability(scriptFile); if (result != null) { + if (result.isMixin()) { + throw new IllegalStateException("Can't reference other mixin scripts!"); + } if (result.requiresReload() || result.clazz == null) { try { return new LookupResult(compilationUnit.addSource(scriptFile.toURI().toURL()), null); @@ -515,7 +549,7 @@ public LookupResult findClassNode(String origName, CompilationUnit compilationUn @Override protected Class recompile(URL source, String className) throws CompilationFailedException, IOException { if (source != null) { - Class c = CustomGroovyScriptEngine.this.onRecompileClass(source, className); + Class c = ScriptEngine.this.onRecompileClass(source, className); if (c != null) { return c; } @@ -535,7 +569,7 @@ public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSourc throw new IllegalStateException("Figure this out"); } if (compiledScript.requiresReload() || compiledScript.clazz == null) { - compiledScript.clazz = CustomGroovyScriptEngine.this.parseDynamicScript(file, false); + compiledScript.clazz = ScriptEngine.this.parseDynamicScript(file, false); } return compiledScript.clazz; } @@ -548,7 +582,7 @@ public Class parseClassRaw(GroovyCodeSource source) { } public Class parseClassRaw(File file) throws IOException { - return parseClassRaw(new GroovyCodeSource(file, CustomGroovyScriptEngine.this.config.getSourceEncoding())); + return parseClassRaw(new GroovyCodeSource(file, ScriptEngine.this.config.getSourceEncoding())); } public Class parseClassRaw(final String text, final String fileName) throws CompilationFailedException { @@ -559,8 +593,8 @@ public Class parseClassRaw(final String text, final String fileName) throws C private Class doParseClass(GroovyCodeSource codeSource) { // local is kept as hard reference to avoid garbage collection - ThreadLocal localTh = getLocalData(); - LocalData localData = new LocalData(); + ThreadLocal localTh = getLocalData(); + ScriptEngine.LocalData localData = new ScriptEngine.LocalData(); localTh.set(localData); StringSetMap cache = localData.dependencyCache; Class answer = null; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java index 9c5bb722c..0ce1c9cb1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java @@ -63,7 +63,7 @@ public void initDefaults() { banPackage("javax.net"); banPackage("javax.security"); banPackage("javax.script"); - banPackage("org.spongepowered"); + //banPackage("org.spongepowered"); banPackage("zone.rong.mixinbooter"); banPackage("net.minecraftforge.gradle"); banClasses(Runtime.class, ClassLoader.class, Scanner.class); @@ -130,7 +130,11 @@ public boolean isValid(Field field) { } public boolean isValid(ClassNode classNode) { - return this.whiteListedClasses.contains(classNode.name) || (!bannedClasses.contains(classNode.name) && !hasBlacklistAnnotation(classNode.visibleAnnotations) && isValidPackage(classNode.name)); + return isValid(classNode, classNode.name.replace('/', '.')); + } + + public boolean isValid(ClassNode classNode, String name) { + return this.whiteListedClasses.contains(name) || (!bannedClasses.contains(name) && !hasBlacklistAnnotation(classNode.visibleAnnotations) && isValidPackage(name)); } public boolean isValid(Class clazz) { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java index d6c07dbe5..4069facd8 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java @@ -5,7 +5,6 @@ import com.cleanroommc.groovyscript.sandbox.mapper.RemappedCachedField; import com.cleanroommc.groovyscript.sandbox.mapper.RemappedCachedMethod; import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager; -import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; @@ -24,7 +23,17 @@ public class GroovyCodeFactory { public static final String MC_CLASS = "net.minecraft."; - public static final boolean spongeForgeLoaded = Loader.isModLoaded("spongeforge"); + public static final boolean spongeForgeLoaded; + + static { + boolean loaded = false; + try { + Class.forName("org.spongepowered.api.Sponge", false, GroovyCodeFactory.class.getClassLoader()); + loaded = true; + } catch (ClassNotFoundException ignored) { + } + spongeForgeLoaded = loaded; + } private GroovyCodeFactory() {} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptMixinVerifier.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptMixinVerifier.java new file mode 100644 index 000000000..923e2726b --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptMixinVerifier.java @@ -0,0 +1,29 @@ +package com.cleanroommc.groovyscript.sandbox.transformer; + +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.spongepowered.asm.mixin.Mixin; + +public class GroovyScriptMixinVerifier extends CompilationCustomizer { + + private static final ClassNode MIXIN_NODE = ClassHelper.makeCached(Mixin.class); + + public GroovyScriptMixinVerifier() { + super(CompilePhase.CANONICALIZATION); + } + + @Override + public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { + for (AnnotationNode annotation : classNode.getAnnotations(MIXIN_NODE)) { + Expression expr = annotation.getMember("value"); + String s = ""; + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java index 994ea8142..e717e2d85 100644 --- a/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java +++ b/src/main/java/com/cleanroommc/groovyscript/server/features/textureDecoration/TextureDecorationProvider.java @@ -38,7 +38,7 @@ public class TextureDecorationProvider extends DocProvider { public static final int ICON_Y = 0; private static final Map> textures = new Object2ObjectOpenHashMap<>(); - public static final File cacheRoot = new File(SandboxData.getCachePath(), "texdecs"); + public static final File cacheRoot = new File(SandboxData.getCacheBasePath(), "texdecs"); static { cacheRoot.mkdirs(); diff --git a/src/main/resources/mixin.groovyscript.custom.early.json b/src/main/resources/mixin.groovyscript.custom.early.json new file mode 100644 index 000000000..a1f925d24 --- /dev/null +++ b/src/main/resources/mixin.groovyscript.custom.early.json @@ -0,0 +1,10 @@ +{ + "package": "mixins", + "refmap": "mixins.groovyscript.refmap.json", + "plugin": "com.cleanroommc.groovyscript.core.EarlyMixinPlugin", + "target": "@env(DEFAULT)", + "minVersion": "0.8", + "compatibilityLevel": "JAVA_8", + "mixins": [], + "client": [] +} diff --git a/src/main/resources/mixin.groovyscript.custom.late.json b/src/main/resources/mixin.groovyscript.custom.late.json new file mode 100644 index 000000000..466f3c6d5 --- /dev/null +++ b/src/main/resources/mixin.groovyscript.custom.late.json @@ -0,0 +1,10 @@ +{ + "package": "mixins", + "refmap": "mixins.groovyscript.refmap.json", + "plugin": "com.cleanroommc.groovyscript.core.LateMixinPlugin", + "target": "@env(DEFAULT)", + "minVersion": "0.8", + "compatibilityLevel": "JAVA_8", + "mixins": [], + "client": [] +} diff --git a/src/main/resources/mixin.groovyscript.groovy.json b/src/main/resources/mixin.groovyscript.groovy.json new file mode 100644 index 000000000..71a427fe4 --- /dev/null +++ b/src/main/resources/mixin.groovyscript.groovy.json @@ -0,0 +1,22 @@ +{ + "package": "com.cleanroommc.groovyscript.core.mixin.groovy", + "refmap": "mixins.groovyscript.refmap.json", + "target": "@env(PREINIT)", + "minVersion": "0.8", + "compatibilityLevel": "JAVA_8", + "priority": 100000000, + "mixinPriority": 100000000, + "required": true, + "mixins": [ + "AsmDecompilerMixin", + "ClassNodeResolverMixin", + "ClosureMixin", + "CompUnitClassGenMixin", + "Java8Mixin", + "MetaClassImplMixin", + "ModuleNodeAccessor", + "ModuleNodeMixin" + ], + "client": [ + ] +} diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 09c8c8191..ad32e1a80 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -22,14 +22,6 @@ "TileEntityPistonMixin", "VillagerProfessionAccessor", "furnace.TileEntityFurnaceMixin", - "groovy.AsmDecompilerMixin", - "groovy.ClassNodeResolverMixin", - "groovy.ClosureMixin", - "groovy.CompUnitClassGenMixin", - "groovy.Java8Mixin", - "groovy.MetaClassImplMixin", - "groovy.ModuleNodeAccessor", - "groovy.ModuleNodeMixin", "loot.LoadTableEventMixin", "loot.LootPoolAccessor", "loot.LootTableAccessor",