From d7237d546719934d730aecba53bee2edc9788c0e Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 9 Apr 2025 16:45:57 +0200 Subject: [PATCH 01/12] works --- examples/mixins/PlayerListMixin.groovy | 17 + examples/mixins/TestClassMixin.groovy | 14 + examples/runConfig.json | 10 +- examples/test/main.groovy | 4 + .../groovyscript/GroovyScript.java | 1 + .../cleanroommc/groovyscript/TestClass.java | 14 + .../groovyscript/command/GSCommand.java | 1 - .../groovyscript/core/GroovyScriptCore.java | 4 +- .../core/GroovyScriptTransformer.java | 3 + .../core/mixin/groovy/ModuleNodeMixin.java | 1 + .../sandbox/AbstractGroovySandbox.java | 360 ++++++++++++++++++ .../groovyscript/sandbox/GroovyLogImpl.java | 22 +- .../sandbox/GroovyScriptSandbox.java | 326 +--------------- .../groovyscript/sandbox/LoadStage.java | 5 + .../groovyscript/sandbox/MixinSandbox.java | 268 +++++++++++++ .../groovyscript/sandbox/SandboxData.java | 57 ++- .../sandbox/{ => engine}/CompiledClass.java | 62 ++- .../sandbox/{ => engine}/CompiledScript.java | 59 ++- .../{ => engine}/GroovyScriptClassLoader.java | 13 +- .../ScriptEngine.java} | 181 +++++---- .../security/GroovySecurityManager.java | 2 +- .../transformer/GroovyCodeFactory.java | 12 +- .../TextureDecorationProvider.java | 2 +- .../resources/mixin.groovyscript.custom.json | 11 + .../resources/mixin.groovyscript.groovy.json | 21 + src/main/resources/mixin.groovyscript.json | 7 - 26 files changed, 1014 insertions(+), 463 deletions(-) create mode 100644 examples/mixins/PlayerListMixin.groovy create mode 100644 examples/mixins/TestClassMixin.groovy create mode 100644 examples/test/main.groovy create mode 100644 src/main/java/com/cleanroommc/groovyscript/TestClass.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java rename src/main/java/com/cleanroommc/groovyscript/sandbox/{ => engine}/CompiledClass.java (65%) rename src/main/java/com/cleanroommc/groovyscript/sandbox/{ => engine}/CompiledScript.java (74%) rename src/main/java/com/cleanroommc/groovyscript/sandbox/{ => engine}/GroovyScriptClassLoader.java (98%) rename src/main/java/com/cleanroommc/groovyscript/sandbox/{CustomGroovyScriptEngine.java => engine/ScriptEngine.java} (80%) create mode 100644 src/main/resources/mixin.groovyscript.custom.json create mode 100644 src/main/resources/mixin.groovyscript.groovy.json 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/runConfig.json b/examples/runConfig.json index c807aeeaa..014a0b6d1 100644 --- a/examples/runConfig.json +++ b/examples/runConfig.json @@ -4,16 +4,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 f40d97012..334c2bf4b 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -114,6 +114,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()); diff --git a/src/main/java/com/cleanroommc/groovyscript/TestClass.java b/src/main/java/com/cleanroommc/groovyscript/TestClass.java new file mode 100644 index 000000000..f66e20246 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/TestClass.java @@ -0,0 +1,14 @@ +package com.cleanroommc.groovyscript; + +import com.cleanroommc.groovyscript.api.GroovyLog; + +public class TestClass { + + static { + GroovyLog.get().info("Hello we are now initialising TestClass"); + } + + public static void sayHello() { + GroovyLog.get().info("Hello from TestClass"); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java b/src/main/java/com/cleanroommc/groovyscript/command/GSCommand.java index 6a69ffaa8..a2ffe2b27 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/GroovyScriptCore.java b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java index 3ee549fe1..6f2817d19 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,7 @@ public void injectData(Map data) { source = (File) data.getOrDefault("coremodLocation", null); SandboxData.initialize((File) FMLInjectionData.data()[6], LOG); SideOnlyConfig.init(); + MixinSandbox.loadMixins(); } @Override @@ -51,6 +53,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/GroovyScriptTransformer.java b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java index a47c3bd61..447a78b1c 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java @@ -19,6 +19,9 @@ public class GroovyScriptTransformer implements IClassTransformer { @Override public byte[] transform(String name, String transformedName, byte[] bytes) { if (bytes == null) return null; + if (transformedName.contains("PlayerLoggedInEvent") || transformedName.endsWith("PlayerList") || transformedName.endsWith("EntityPlayerMP") || transformedName.endsWith("NetworkManager") || transformedName.endsWith("NetHandlerPlayServer") || transformedName.endsWith("TextComponentString")) { + GroovyScriptCore.LOG.info("Loading class {}", transformedName); + } switch (name) { case InvokerHelperVisitor.CLASS_NAME: { ClassWriter classWriter = new ClassWriter(0); 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/sandbox/AbstractGroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java new file mode 100644 index 000000000..7563d0116 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java @@ -0,0 +1,360 @@ +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.event.GroovyEventManager; +import com.cleanroommc.groovyscript.event.GroovyReloadEvent; +import com.cleanroommc.groovyscript.event.ScriptRunEvent; +import com.cleanroommc.groovyscript.helper.Alias; +import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; +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 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 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 { + Collection files = getScriptFiles(); + List scripts = this.engine.findScripts(files); + 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(); + // first clear all added events + GroovyEventManager.INSTANCE.reset(); + if (this.currentLoadStage.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); + // and finally invoke pre script run event + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(this.currentLoadStage)); + } + + @ApiStatus.OverrideOnly + protected boolean shouldRunFile(String file) { + return true; + } + + @ApiStatus.OverrideOnly + protected void postRun() { + if (this.currentLoadStage == LoadStage.POST_INIT) { + ReloadableRegistryManager.afterScriptRun(); + } + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(this.currentLoadStage)); + if (this.currentLoadStage == LoadStage.POST_INIT && ReloadableRegistryManager.isFirstLoad()) { + ReloadableRegistryManager.setLoaded(); + } + } + + 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/GroovyLogImpl.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java index 020e1d4d7..e366bd3f8 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java @@ -6,12 +6,13 @@ 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 +40,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()); } @@ -299,7 +305,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 + Side side = this.passedEarly ? FMLCommonHandler.instance().getEffectiveSide() : FMLLaunchHandler.side(); + return side.isClient() ? "CLIENT" : "SERVER"; } 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 3545b98ab..2f0791074 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -1,64 +1,27 @@ 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.registry.ReloadableRegistryManager; +import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; -import groovy.lang.Binding; -import groovy.lang.Closure; -import groovy.lang.GroovyRuntimeException; -import groovy.lang.Script; -import groovy.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); getImportCustomizer().addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName()); getImportCustomizer().addImports( @@ -92,272 +55,13 @@ 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) { - 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 - protected void preRun() { - if (CustomGroovyScriptEngine.DELETE_CACHE_ON_RUN) this.engine.deleteScriptCache(); - // first clear all added events - GroovyEventManager.INSTANCE.reset(); - if (this.currentLoadStage.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); - // and finally invoke pre script run event - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(this.currentLoadStage)); - } - - @ApiStatus.OverrideOnly - protected boolean shouldRunFile(String file) { - return true; - } - - @ApiStatus.OverrideOnly - protected void postRun() { - if (this.currentLoadStage == LoadStage.POST_INIT) { - ReloadableRegistryManager.afterScriptRun(); - } - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(this.currentLoadStage)); - if (this.currentLoadStage == 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; + @Override + public boolean canRunInStage(LoadStage stage) { + return !stage.isMixin(); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java index a6fb20165..4c4fbefb2 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java @@ -6,6 +6,7 @@ public enum LoadStage { + MIXIN("mixin", false, -1000000000), PRE_INIT("preInit", false, -1000000), INIT("init", false, -1000), POST_INIT("postInit", true, 0); @@ -42,6 +43,10 @@ public int getPriority() { return priority; } + public boolean isMixin() { + return this == MIXIN; + } + @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..0448f5016 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -0,0 +1,268 @@ +package com.cleanroommc.groovyscript.sandbox; + +import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.sandbox.engine.CompiledClass; +import com.cleanroommc.groovyscript.sandbox.engine.CompiledScript; +import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.ModifyReceiver; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import groovy.lang.Binding; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.Script; +import groovyjarjarasm.asm.ClassVisitor; +import groovyjarjarasm.asm.ClassWriter; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.*; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.codehaus.groovy.control.messages.ExceptionMessage; +import org.codehaus.groovy.syntax.SyntaxException; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.extensibility.IMixinConfig; +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.transformer.Config; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.*; + +public class MixinSandbox extends AbstractGroovySandbox { + + @ApiStatus.Internal + public static void loadMixins() { + if (instance != null) { + throw new IllegalStateException("Mixins already loaded"); + } + instance = new MixinSandbox(); + instance.run(LoadStage.MIXIN); + + // 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 = instance.collectCompiledMixins(); + if (groovyMixins.isEmpty()) { + LOG.info("No groovy mixins configured"); + return; + } + String cfgName = "mixin.groovyscript.custom.json"; + // create and register config + Mixins.addConfiguration(cfgName); + // obtain the just created config + IMixinConfig config = Config.create(cfgName, MixinEnvironment.getDefaultEnvironment()).getConfig(); + List mixinClasses; + try { + Class mixinConfigClass = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); + Field field = mixinConfigClass.getDeclaredField("mixinClasses"); + field.setAccessible(true); + mixinClasses = (List) field.get(config); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + // inject loaded mixin classes into configuration + mixinClasses.addAll(groovyMixins); + instance.getEngine().writeIndex();*/ + } + + private static MixinSandbox instance; + private static final Map resourceCache; + + static { + Map resourceCache1; + try { + Field field = LaunchClassLoader.class.getDeclaredField("resourceCache"); + field.setAccessible(true); + resourceCache1 = (Map) field.get(Launch.classLoader); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + resourceCache = resourceCache1; + } + + public static final Logger LOG = LogManager.getLogger("GroovyScript-MixinSandbox"); + public static final boolean DEBUG = true; + + private MixinSandbox() {} + + @Override + protected ScriptEngine createEngine(CompilerConfiguration config) { + return new ScriptEngine(SandboxData.getRootUrls(), SandboxData.getMixinScriptCachePath(), SandboxData.getScriptFile(), config) + .onClassLoaded(cc -> { + GroovyLog.get().info(" - loaded mixin class {} from cache", cc.getName()); + resourceCache.put(cc.getName(), cc.getData()); + }); + } + + @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) + .stream() + .map(Class::getName) + .toArray(String[]::new)); + super.initConfig(config); + config.addCompilationCustomizers(new CallbackInjector()); + } + + @Override + public boolean canRunInStage(LoadStage stage) { + return stage.isMixin(); + } + + @Override + protected void preRun() { + if (ScriptEngine.DELETE_CACHE_ON_RUN) getEngine().deleteScriptCache(); + GroovyLog.get().infoMC("Running scripts in loader '{}'", getCurrentLoader()); + } + + @Override + protected void postRun() {} + + private Collection collectCompiledMixins() { + List mixinClasses = new ArrayList<>(); + Iterator it = getEngine().classIterator(); + while (it.hasNext()) { + CompiledClass compiledClass = it.next(); + if (compiledClass.hasData()) { + if (DEBUG) LOG.info("found groovy mixin class {}", compiledClass.getName()); + mixinClasses.add(compiledClass.getName().substring(SandboxData.MIXIN_PKG.length() + 1)); + } + } + return mixinClasses; + } + + @Override + public Collection getScriptFiles() { + return SandboxData.getSortedFilesOf(getScriptRoot(), Collections.singleton(SandboxData.MIXIN_PKG + "/")); + } + + @Override + protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) { + long t = System.currentTimeMillis(); + //getEngine().loadScript(compiledScript); + this.compileTime += System.currentTimeMillis() - t; + } + + private String toClassName(String path) { + int i = path.lastIndexOf('.'); + if (i < 0) { + LOG.error("Path must end with '.groovy', but was '{}'", path); + return null; + } + return path.substring(0, i).replace('/', '.'); + } + + private class ClassGenerator implements CompilationUnit.ClassgenCallback { + + private final SourceUnit su; + + private ClassGenerator(SourceUnit su) { + this.su = su; + } + + @Override + public void call(ClassVisitor classVisitor, ClassNode classNode) throws CompilationFailedException { + // mixin will try to find the bytes in that map, so we will put them there + byte[] code = ((ClassWriter) classVisitor).toByteArray(); + resourceCache.put(classNode.getName(), code); + if (DEBUG) LOG.info("Generated groovy mixin class {}", classNode.getName()); + MixinSandbox.this.getEngine().onCompileClass(this.su, classNode, null, code); + } + } + + private class CallbackInjector extends CompilationCustomizer { + + public CallbackInjector() { + super(CompilePhase.CANONICALIZATION); + } + + public void call(CompilationUnit cu, SourceUnit su, ClassNode classNode) throws CompilationFailedException { + if (cu.getClassgenCallback() instanceof ClassGenerator) { + GroovyLog.get().infoMC(" overwriting ClassGenerator with su {} and class {}", su.getName(), classNode.getName()); + } + cu.setClassgenCallback(new ClassGenerator(su)); + } + + private void changeBugText(final GroovyBugError e, final SourceUnit context, CompilationUnit unit) { + e.setBugText("exception in phase '" + unit.getPhaseDescription() + "' in source unit '" + (context != null ? context.getName() + : "?") + "' " + e.getBugText()); + } + + @Override + public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { + throw new UnsupportedOperationException(); + } + + @Override + public void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException { + for (ModuleNode module : unit.getAST().getModules()) { + for (ClassNode classNode : module.getClasses()) { + SourceUnit context = null; + try { + context = classNode.getModule().getContext(); + if (context == null || context.getPhase() < unit.getPhase() || (context.getPhase() == unit.getPhase() && !context.isPhaseComplete())) { + call(unit, context, classNode); + } + } catch (CompilationFailedException e) { + // fall through + } catch (NullPointerException npe) { + GroovyBugError gbe = new GroovyBugError("unexpected NullPointerException", npe); + changeBugText(gbe, context, unit); + throw gbe; + } catch (GroovyBugError e) { + changeBugText(e, context, unit); + throw e; + } catch (Exception | LinkageError e) { + ErrorCollector errorCollector = null; + // check for a nested compilation exception + for (Throwable t = e.getCause(); t != e && t != null; t = t.getCause()) { + if (t instanceof MultipleCompilationErrorsException) { + errorCollector = ((MultipleCompilationErrorsException) t).getErrorCollector(); + break; + } + } + + if (errorCollector != null) { + unit.getErrorCollector().addCollectorContents(errorCollector); + } else { + if (e instanceof GroovyRuntimeException) { + GroovyRuntimeException gre = (GroovyRuntimeException) e; + context = Optional.ofNullable(gre.getModule()).map(ModuleNode::getContext).orElse(context); + } + if (context != null) { + if (e instanceof SyntaxException) { + unit.getErrorCollector().addError((SyntaxException) e, context); + } else if (e.getCause() instanceof SyntaxException) { + unit.getErrorCollector().addError((SyntaxException) e.getCause(), context); + } else { + unit.getErrorCollector().addException(e instanceof Exception ? (Exception) e : new RuntimeException(e), context); + } + } else { + unit.getErrorCollector().addError(new ExceptionMessage( + e instanceof Exception ? (Exception) e : new RuntimeException(e), false, unit)); + } + } + } + } + } + if (unit.getErrorCollector().hasErrors()) { + throw new MultipleCompilationErrorsException(unit.getErrorCollector()); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java index 384573f6b..5ae206375 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/SandboxData.java @@ -13,10 +13,7 @@ import java.net.URL; 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.util.*; import java.util.stream.Stream; /** @@ -24,16 +21,21 @@ */ public class SandboxData { + public static final String MIXIN_PKG = "mixins"; + public static final String[] GROOVY_SUFFIXES = { ".groovy", ".gvy", ".gy", ".gsh" }; 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 boolean initialised = false; private SandboxData() {} @@ -48,7 +50,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"); @@ -61,12 +65,15 @@ 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); @@ -88,6 +95,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; @@ -98,14 +110,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 standardScriptCachePath; + } + + public static @NotNull File getMixinScriptCachePath() { ensureLoaded(); - return rootUrl; + return mixinScriptCachePath; + } + + public static @NotNull URL getRootUrl() { + return getRootUrls()[0]; } public static @NotNull URL[] getRootUrls() { @@ -113,6 +134,15 @@ 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; + } + private static void ensureLoaded() { if (!initialised) { throw new IllegalStateException("Sandbox data is not yet Initialised."); @@ -162,6 +192,7 @@ static Collection getSortedFilesOf(File root, Collection paths) { 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 65% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java index c65240e41..eaca86b0d 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CompiledClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java @@ -1,9 +1,13 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.sandbox.FileUtil; import groovy.lang.GroovyClassLoader; 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; @@ -11,34 +15,34 @@ import java.nio.file.Files; import java.util.Map; -class CompiledClass { +@ApiStatus.Internal +public class CompiledClass { public static final String CLASS_SUFFIX = ".clz"; final String path; - String name; + final String name; byte[] data; Class clazz; + final boolean mixin; public CompiledClass(String path, String name) { this.path = path; this.name = name; + this.mixin = name.startsWith("mixin"); } - public void onCompile(byte[] data, Class clazz, String basePath) { + public void onCompile(byte @NotNull [] data, @Nullable Class clazz, String basePath) { this.data = data; - onCompile(clazz, basePath); - } - - public void onCompile(Class clazz, String basePath) { this.clazz = clazz; - if (!this.name.equals(clazz.getName())) throw new IllegalArgumentException(); - //this.name = clazz.getName(); + if (clazz != null && !this.name.equals(clazz.getName())) { + throw new IllegalArgumentException("Expected class name to be " + this.name + ", but was " + clazz.getName()); + } if (this.data == null) { GroovyLog.get().errorMC("The class doesnt seem to be compiled yet. (" + name + ")"); return; } - if (!CustomGroovyScriptEngine.ENABLE_CACHE) return; + if (!ScriptEngine.ENABLE_CACHE) return; try { File file = getDataFile(basePath); file.getParentFile().mkdirs(); @@ -51,15 +55,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 +74,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 +97,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 74% 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..a00ddc758 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.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.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; 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('.'); @@ -42,9 +46,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,19 +62,6 @@ 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); @@ -125,6 +116,40 @@ 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.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(); 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 98% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java index fce4c9fb8..be908251d 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java @@ -1,4 +1,4 @@ -package com.cleanroommc.groovyscript.sandbox; +package com.cleanroommc.groovyscript.sandbox.engine; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyCodeSource; @@ -290,6 +290,11 @@ protected GroovyClassLoader.ClassCollector createCollector(CompilationUnit unit, throw new UnsupportedOperationException(); } + public interface ClassgenCallback { + + void onGenerate(byte[] bytes, ClassNode classNode, Class clz); + } + public static class ClassCollector implements CompilationUnit.ClassgenCallback { private Class generatedClass; @@ -297,7 +302,7 @@ public static class ClassCollector implements CompilationUnit.ClassgenCallback { private final SourceUnit su; private final CompilationUnit unit; private final Collection> loadedClasses; - private BiConsumer> creatClassCallback; + private ClassgenCallback creatClassCallback; protected ClassCollector(GroovyScriptClassLoader cl, CompilationUnit unit, SourceUnit su) { this.cl = cl; @@ -330,7 +335,7 @@ protected Class createClass(byte[] code, ClassNode classNode) { } if (this.creatClassCallback != null) { - this.creatClassCallback.accept(code, theClass); + this.creatClassCallback.onGenerate(code, classNode, theClass); } return theClass; } @@ -349,7 +354,7 @@ public Collection> getLoadedClasses() { return this.loadedClasses; } - public ClassCollector creatClassCallback(BiConsumer> creatClassCallback) { + public ClassCollector creatClassCallback(ClassgenCallback creatClassCallback) { this.creatClassCallback = creatClassCallback; return this; } 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 80% rename from src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java rename to src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java index 701be4738..9e70c0f25 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/CustomGroovyScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java @@ -1,8 +1,9 @@ -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; @@ -17,7 +18,6 @@ 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; @@ -36,8 +36,9 @@ import java.net.URLConnection; import java.security.CodeSource; import java.util.*; +import java.util.function.Consumer; -public class CustomGroovyScriptEngine implements ResourceConnector { +public class ScriptEngine implements ResourceConnector { /** * Changing this number will force the cache to be deleted and every script has to be recompiled. @@ -72,13 +73,14 @@ private static synchronized ThreadLocal getLocalData() { private final ScriptClassLoader classLoader; private final Map index = new Object2ObjectOpenHashMap<>(); private final Map loadedClasses = new Object2ObjectOpenHashMap<>(); + private Consumer onClassLoaded; - 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 = new ScriptClassLoader(ScriptEngine.class.getClassLoader(), config, Collections.unmodifiableMap(this.loadedClasses)); readIndex(); } @@ -98,26 +100,49 @@ public GroovyScriptClassLoader getClassLoader() { return classLoader; } - public Iterable> getAllLoadedScriptClasses() { - return () -> new AbstractIterator<>() { + public ScriptEngine onClassLoaded(Consumer onClassLoaded) { + this.onClassLoaded = onClassLoaded; + return this; + } + + 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(); @@ -151,7 +176,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!!!"); @@ -180,7 +206,33 @@ 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); + if (this.onClassLoaded != null) { + this.onClassLoaded.accept(cc); + } + } + } + + @ApiStatus.Internal + public List findScripts(Collection files) { List scripts = new ArrayList<>(files.size()); for (File file : files) { CompiledScript cs = checkScriptLoadability(file); @@ -189,11 +241,12 @@ List findScripts(Collection files) { return scripts; } - void loadScript(CompiledScript script) { + @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; @@ -212,45 +265,17 @@ CompiledScript checkScriptLoadability(File file) { String relativeFileName = FileUtil.relativize(this.scriptRoot.getPath(), file.getPath()); 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; } @@ -314,20 +339,21 @@ private File findScriptFile(String scriptName) { * 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) { + public void onCompileClass(@NotNull SourceUnit su, @NotNull ClassNode classNode, @Nullable Class clazz, byte @NotNull [] code) { + String path = su.getName(); String shortPath = FileUtil.relativize(this.scriptRoot.getPath(), path); + String className = classNode.getName(); + 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()); + if (inner) innerClass = comp.findInnerClass(className); innerClass.onCompile(code, clazz, this.cacheRoot.getPath()); this.loadedClasses.put(innerClass.name, innerClass); } @@ -344,17 +370,26 @@ 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; + public static boolean isInnerClass(String className) { + int i = className.lastIndexOf('.'); + if (i < 0) i = 0; + return className.indexOf('$', i + 1) >= 0; } + 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 { @@ -436,7 +471,7 @@ public ScriptClassLoader(ClassLoader loader, CompilerConfiguration config, Map { - onCompileClass(su, su.getName(), clz, code, clz.getName().contains("$")); + return super.createCustomCollector(unit, su).creatClassCallback((code, classNode, clz) -> { + onCompileClass(su, classNode, clz, code); }); } @@ -463,7 +498,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 */ } @@ -485,7 +520,7 @@ 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.requiresReload() || result.clazz == null) { @@ -508,7 +543,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; } @@ -528,7 +563,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; } @@ -541,7 +576,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 { @@ -552,8 +587,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..4ea93127d 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); 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..aab41f715 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyCodeFactory.java @@ -24,7 +24,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/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.json b/src/main/resources/mixin.groovyscript.custom.json new file mode 100644 index 000000000..a12d5e939 --- /dev/null +++ b/src/main/resources/mixin.groovyscript.custom.json @@ -0,0 +1,11 @@ +{ + "package": "mixins", + "refmap": "mixins.groovyscript.refmap.json", + "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..ca02a8dee --- /dev/null +++ b/src/main/resources/mixin.groovyscript.groovy.json @@ -0,0 +1,21 @@ +{ + "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", + "ClosureMixin", + "CompUnitClassGenMixin", + "Java8Mixin", + "MetaClassImplMixin", + "ModuleNodeAccessor", + "ModuleNodeMixin" + ], + "client": [ + ] +} diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 5ca5064f1..8bfb215e3 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -22,13 +22,6 @@ "SlotCraftingAccess", "TileEntityPistonMixin", "VillagerProfessionAccessor", - "groovy.AsmDecompilerMixin", - "groovy.ClosureMixin", - "groovy.CompUnitClassGenMixin", - "groovy.Java8Mixin", - "groovy.MetaClassImplMixin", - "groovy.ModuleNodeAccessor", - "groovy.ModuleNodeMixin", "loot.LoadTableEventMixin", "loot.LootPoolAccessor", "loot.LootTableAccessor", From 07e76ff4ff4a741bef535d8616b7d778132fa7b9 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 9 Apr 2025 16:52:36 +0200 Subject: [PATCH 02/12] forgor --- .../com/cleanroommc/groovyscript/sandbox/MixinSandbox.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index 0448f5016..d696203e2 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -51,7 +51,7 @@ public static void loadMixins() { // 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 = instance.collectCompiledMixins(); + Collection groovyMixins = instance.collectCompiledMixins(); if (groovyMixins.isEmpty()) { LOG.info("No groovy mixins configured"); return; @@ -72,7 +72,7 @@ public static void loadMixins() { } // inject loaded mixin classes into configuration mixinClasses.addAll(groovyMixins); - instance.getEngine().writeIndex();*/ + instance.getEngine().writeIndex(); } private static MixinSandbox instance; From dcda612debd151b88e9524ac8295eff3275a30ed Mon Sep 17 00:00:00 2001 From: brachy84 Date: Wed, 9 Apr 2025 18:28:17 +0200 Subject: [PATCH 03/12] clean up --- .../sandbox/AbstractGroovySandbox.java | 29 +--- .../sandbox/GroovyScriptSandbox.java | 32 +++++ .../groovyscript/sandbox/MixinSandbox.java | 130 ++++++++---------- .../sandbox/engine/CompiledClass.java | 7 - .../sandbox/engine/CompiledScript.java | 3 - .../engine/GroovyScriptClassLoader.java | 7 +- .../sandbox/engine/ScriptEngine.java | 39 +----- .../transformer/GroovyCodeFactory.java | 1 - 8 files changed, 102 insertions(+), 146 deletions(-) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java index 7563d0116..0e922a942 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java @@ -4,11 +4,7 @@ import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.api.INamed; -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.registry.ReloadableRegistryManager; import com.cleanroommc.groovyscript.sandbox.engine.CompiledScript; import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; import groovy.lang.Binding; @@ -19,7 +15,6 @@ import groovy.util.ScriptException; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -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; @@ -232,7 +227,7 @@ protected void loadScripts(Binding binding, Set executedClasses, boolean } } - protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) throws Throwable{ + protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) throws Throwable { long t = System.currentTimeMillis(); this.engine.loadScript(compiledScript); this.compileTime += System.currentTimeMillis() - t; @@ -280,18 +275,6 @@ protected void initConfig(CompilerConfiguration config) { @ApiStatus.OverrideOnly protected void preRun() { if (ScriptEngine.DELETE_CACHE_ON_RUN) this.engine.deleteScriptCache(); - // first clear all added events - GroovyEventManager.INSTANCE.reset(); - if (this.currentLoadStage.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); - // and finally invoke pre script run event - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(this.currentLoadStage)); } @ApiStatus.OverrideOnly @@ -300,15 +283,7 @@ protected boolean shouldRunFile(String file) { } @ApiStatus.OverrideOnly - protected void postRun() { - if (this.currentLoadStage == LoadStage.POST_INIT) { - ReloadableRegistryManager.afterScriptRun(); - } - MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(this.currentLoadStage)); - if (this.currentLoadStage == LoadStage.POST_INIT && ReloadableRegistryManager.isFirstLoad()) { - ReloadableRegistryManager.setLoaded(); - } - } + protected void postRun() {} public File getScriptRoot() { return getEngine().getScriptRoot(); diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index 2f0791074..c80531771 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -3,11 +3,15 @@ import com.cleanroommc.groovyscript.api.GroovyLog; 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.GroovyHelper; +import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptEarlyCompiler; import net.minecraft.util.math.MathHelper; +import net.minecraftforge.common.MinecraftForge; import org.codehaus.groovy.control.CompilerConfiguration; public class GroovyScriptSandbox extends AbstractGroovySandbox { @@ -64,4 +68,32 @@ protected void initConfig(CompilerConfiguration config) { public boolean canRunInStage(LoadStage stage) { return !stage.isMixin(); } + + @Override + protected void preRun() { + super.preRun(); + // first clear all added events + GroovyEventManager.INSTANCE.reset(); + 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 '{}'", getCurrentLoader()); + // this.engine.prepareEngine(this.currentLoadStage); + // and finally invoke pre script run event + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Pre(getCurrentLoader())); + } + + @Override + protected void postRun() { + if (getCurrentLoader() == LoadStage.POST_INIT) { + ReloadableRegistryManager.afterScriptRun(); + } + MinecraftForge.EVENT_BUS.post(new ScriptRunEvent.Post(getCurrentLoader())); + if (getCurrentLoader() == LoadStage.POST_INIT && ReloadableRegistryManager.isFirstLoad()) { + ReloadableRegistryManager.setLoaded(); + } + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index d696203e2..ab98b850b 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -11,7 +11,6 @@ import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.Share; import groovy.lang.Binding; -import groovy.lang.GroovyRuntimeException; import groovy.lang.Script; import groovyjarjarasm.asm.ClassVisitor; import groovyjarjarasm.asm.ClassWriter; @@ -25,8 +24,6 @@ import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.*; import org.codehaus.groovy.control.customizers.CompilationCustomizer; -import org.codehaus.groovy.control.messages.ExceptionMessage; -import org.codehaus.groovy.syntax.SyntaxException; import org.jetbrains.annotations.ApiStatus; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; @@ -106,15 +103,44 @@ protected ScriptEngine createEngine(CompilerConfiguration config) { @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) - .stream() - .map(Class::getName) - .toArray(String[]::new)); + 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) + .stream() + .map(Class::getName) + .toArray(String[]::new)); super.initConfig(config); config.addCompilationCustomizers(new CallbackInjector()); } @@ -133,6 +159,16 @@ protected void preRun() { @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!"); + } + private Collection collectCompiledMixins() { List mixinClasses = new ArrayList<>(); Iterator it = getEngine().classIterator(); @@ -154,19 +190,10 @@ public Collection getScriptFiles() { @Override protected void loadScript(CompiledScript compiledScript, Binding binding, boolean run) { long t = System.currentTimeMillis(); - //getEngine().loadScript(compiledScript); + getEngine().loadScript(compiledScript); this.compileTime += System.currentTimeMillis() - t; } - private String toClassName(String path) { - int i = path.lastIndexOf('.'); - if (i < 0) { - LOG.error("Path must end with '.groovy', but was '{}'", path); - return null; - } - return path.substring(0, i).replace('/', '.'); - } - private class ClassGenerator implements CompilationUnit.ClassgenCallback { private final SourceUnit su; @@ -193,76 +220,37 @@ public CallbackInjector() { public void call(CompilationUnit cu, SourceUnit su, ClassNode classNode) throws CompilationFailedException { if (cu.getClassgenCallback() instanceof ClassGenerator) { + // this is a test to see if it is called multiple times on the same unit GroovyLog.get().infoMC(" overwriting ClassGenerator with su {} and class {}", su.getName(), classNode.getName()); } cu.setClassgenCallback(new ClassGenerator(su)); } private void changeBugText(final GroovyBugError e, final SourceUnit context, CompilationUnit unit) { - e.setBugText("exception in phase '" + unit.getPhaseDescription() + "' in source unit '" + (context != null ? context.getName() - : "?") + "' " + e.getBugText()); + e.setBugText( + "exception in phase '" + unit.getPhaseDescription() + "' in source unit '" + (context != null + ? context.getName() + : "?") + "' " + e.getBugText()); } @Override public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { + // we cant use this since we need the compile unit throw new UnsupportedOperationException(); } @Override public void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException { + // is all this looping really needed? for (ModuleNode module : unit.getAST().getModules()) { for (ClassNode classNode : module.getClasses()) { SourceUnit context = null; - try { - context = classNode.getModule().getContext(); - if (context == null || context.getPhase() < unit.getPhase() || (context.getPhase() == unit.getPhase() && !context.isPhaseComplete())) { - call(unit, context, classNode); - } - } catch (CompilationFailedException e) { - // fall through - } catch (NullPointerException npe) { - GroovyBugError gbe = new GroovyBugError("unexpected NullPointerException", npe); - changeBugText(gbe, context, unit); - throw gbe; - } catch (GroovyBugError e) { - changeBugText(e, context, unit); - throw e; - } catch (Exception | LinkageError e) { - ErrorCollector errorCollector = null; - // check for a nested compilation exception - for (Throwable t = e.getCause(); t != e && t != null; t = t.getCause()) { - if (t instanceof MultipleCompilationErrorsException) { - errorCollector = ((MultipleCompilationErrorsException) t).getErrorCollector(); - break; - } - } - - if (errorCollector != null) { - unit.getErrorCollector().addCollectorContents(errorCollector); - } else { - if (e instanceof GroovyRuntimeException) { - GroovyRuntimeException gre = (GroovyRuntimeException) e; - context = Optional.ofNullable(gre.getModule()).map(ModuleNode::getContext).orElse(context); - } - if (context != null) { - if (e instanceof SyntaxException) { - unit.getErrorCollector().addError((SyntaxException) e, context); - } else if (e.getCause() instanceof SyntaxException) { - unit.getErrorCollector().addError((SyntaxException) e.getCause(), context); - } else { - unit.getErrorCollector().addException(e instanceof Exception ? (Exception) e : new RuntimeException(e), context); - } - } else { - unit.getErrorCollector().addError(new ExceptionMessage( - e instanceof Exception ? (Exception) e : new RuntimeException(e), false, unit)); - } - } + context = classNode.getModule().getContext(); + if (context == null || context.getPhase() < unit.getPhase() || (context.getPhase() == unit.getPhase() && !context.isPhaseComplete())) { + call(unit, context, classNode); } } } - if (unit.getErrorCollector().hasErrors()) { - throw new MultipleCompilationErrorsException(unit.getErrorCollector()); - } } } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java index eaca86b0d..bc77c0b93 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java @@ -1,8 +1,6 @@ package com.cleanroommc.groovyscript.sandbox.engine; -import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.sandbox.FileUtil; -import groovy.lang.GroovyClassLoader; import org.apache.commons.lang3.builder.ToStringBuilder; import org.codehaus.groovy.runtime.InvokerHelper; import org.jetbrains.annotations.ApiStatus; @@ -13,7 +11,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.util.Map; @ApiStatus.Internal public class CompiledClass { @@ -38,10 +35,6 @@ public void onCompile(byte @NotNull [] data, @Nullable Class clazz, String ba if (clazz != null && !this.name.equals(clazz.getName())) { throw new IllegalArgumentException("Expected class name to be " + this.name + ", but was " + clazz.getName()); } - if (this.data == null) { - GroovyLog.get().errorMC("The class doesnt seem to be compiled yet. (" + name + ")"); - return; - } if (!ScriptEngine.ENABLE_CACHE) return; try { File file = getDataFile(basePath); diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java index a00ddc758..7719679c5 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java @@ -1,12 +1,10 @@ package com.cleanroommc.groovyscript.sandbox.engine; -import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.helper.JsonHelper; 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.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -15,7 +13,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Map; @ApiStatus.Internal public class CompiledScript extends CompiledClass { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java index be908251d..92a728c3d 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java @@ -3,7 +3,6 @@ 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; } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java index 9e70c0f25..6e170c403 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java @@ -9,7 +9,6 @@ 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; @@ -38,7 +37,7 @@ import java.util.*; import java.util.function.Consumer; -public class ScriptEngine implements ResourceConnector { +public class ScriptEngine { /** * Changing this number will force the cache to be deleted and every script has to be recompiled. @@ -340,6 +339,7 @@ private File findScriptFile(String scriptName) { */ @ApiStatus.Internal public void onCompileClass(@NotNull SourceUnit su, @NotNull ClassNode classNode, @Nullable Class clazz, byte @NotNull [] code) { + // class is null for mixins String path = su.getName(); String shortPath = FileUtil.relativize(this.scriptRoot.getPath(), path); String className = classNode.getName(); @@ -348,8 +348,7 @@ public void onCompileClass(@NotNull SourceUnit su, @NotNull ClassNode classNode, // we need to find the source unit of the compiled class SourceUnit trueSource = su.getAST().getUnit().getScriptSourceLocation(mainClassName); String truePath = trueSource == null ? shortPath : FileUtil.relativize(this.scriptRoot.getPath(), trueSource.getName()); - boolean inner = className.length() != mainClassName.length() || - (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(className)); + boolean inner = className.length() != mainClassName.length() || (shortPath.equals(truePath) && su.getAST().getMainClassName() != null && !su.getAST().getMainClassName().equals(className)); CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, inner ? -1 : 0)); CompiledClass innerClass = comp; @@ -377,12 +376,6 @@ public Class onRecompileClass(URL source, String className) { return c; } - public static boolean isInnerClass(String className) { - int i = className.lastIndexOf('.'); - if (i < 0) i = 0; - return className.indexOf('$', i + 1) >= 0; - } - private static String mainClassName(String className) { int i = className.lastIndexOf('.'); if (i < 0) i = 0; @@ -391,8 +384,7 @@ private static String mainClassName(String 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; @@ -431,28 +423,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 { 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 aab41f715..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; From 58f0013258351ccb4c3355ae49410e13c84b655d Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 11 May 2025 21:34:47 +0200 Subject: [PATCH 04/12] update build script --- build.gradle | 10 +++++----- gradle.properties | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 65d0de86f..4a9686dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -//version: 1723428048 +//version: 1743737794 /* * DO NOT CHANGE THIS FILE! * Also, you may replace this file at any time if there is an update available. @@ -80,6 +80,7 @@ propertyDefaultIfUnset("includeWellKnownRepositories", true) propertyDefaultIfUnset("includeCommonDevEnvMods", true) propertyDefaultIfUnset("stripForgeRequirements", false) propertyDefaultIfUnset("noPublishedSources", false) +propertyDefaultIfUnset("mixinProviderSpec", "zone.rong:mixinbooter:10.6") propertyDefaultIfUnset("forceEnableMixins", false) propertyDefaultIfUnset("mixinConfigRefmap", "mixins.${project.modId}.refmap.json") propertyDefaultIfUnsetWithEnvVar("enableCoreModDebug", false, "CORE_MOD_DEBUG") @@ -518,7 +519,6 @@ configurations { testRuntimeClasspath.extendsFrom(runtimeOnlyNonPublishable) } -String mixinProviderSpec = 'zone.rong:mixinbooter:9.1' dependencies { if (usesMixins.toBoolean()) { annotationProcessor 'org.ow2.asm:asm-debug-all:5.2' @@ -871,9 +871,9 @@ if (enableJava17RunTasks.toBoolean()) { dependencies { if (modId != 'lwjgl3ify') { java17Dependencies("io.github.twilightflower:lwjgl3ify:1.0.0") - } - java17PatchDependencies("io.github.twilightflower:lwjgl3ify:1.0.0:forgePatches") { + java17PatchDependencies("io.github.twilightflower:lwjgl3ify:1.0.0:forgePatches") { transitive = false + } } } @@ -1009,7 +1009,7 @@ abstract class RunHotswappableMinecraftTask extends RunMinecraftTask { if (project.usesMixins.toBoolean()) { this.extraJvmArgs.addAll(project.provider(() -> { - def mixinCfg = project.configurations.detachedConfiguration(project.dependencies.create(project.mixinProviderSpec)) + def mixinCfg = project.configurations.detachedConfiguration(project.dependencies.create(mixinProviderSpec)) mixinCfg.canBeConsumed = false mixinCfg.canBeResolved = true mixinCfg.transitive = false diff --git a/gradle.properties b/gradle.properties index 10ffb67fd..9d598849d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -184,6 +184,8 @@ mixinsPackage = core.mixin mixinConfigRefmap = # Automatically generates a mixin config json if enabled, with the name mixins.modid.json generateMixinConfig = false +# Mixin provider specification +mixinProviderSpec = zone.rong:mixinbooter:10.7 # Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! # Example value: coreModClass = asm.FMLPlugin + modGroup = com.myname.mymodid -> com.myname.mymodid.asm.FMLPlugin coreModClass = core.GroovyScriptCore From 0ca665bf471b1fff734c869f6f2212569dc62ed6 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 11 May 2025 21:35:54 +0200 Subject: [PATCH 05/12] missing auto imports --- .../sandbox/GroovyScriptSandbox.java | 2 +- .../groovyscript/sandbox/MixinSandbox.java | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index 4f09bc636..fc33fe10e 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -8,8 +8,8 @@ import com.cleanroommc.groovyscript.helper.GroovyHelper; import com.cleanroommc.groovyscript.helper.MetaClassExpansion; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; -import com.cleanroommc.groovyscript.sandbox.expand.ExpansionHelper; import com.cleanroommc.groovyscript.sandbox.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.*; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index ab98b850b..ed041ee83 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -7,9 +7,11 @@ 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 groovyjarjarasm.asm.ClassVisitor; @@ -27,9 +29,12 @@ import org.jetbrains.annotations.ApiStatus; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; +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 org.spongepowered.asm.mixin.transformer.Config; import java.io.File; @@ -137,7 +142,19 @@ protected void initConfig(CompilerConfiguration config) { ModifyReceiver.class, ModifyReturnValue.class, Local.class, - Share.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)); From 262a39f4543d969bbef873e0f0de61f01bf4cdbb Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 11 May 2025 21:39:54 +0200 Subject: [PATCH 06/12] fix mixin prio --- src/main/resources/mixin.groovyscript.groovy.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/mixin.groovyscript.groovy.json b/src/main/resources/mixin.groovyscript.groovy.json index cd9e95399..71a427fe4 100644 --- a/src/main/resources/mixin.groovyscript.groovy.json +++ b/src/main/resources/mixin.groovyscript.groovy.json @@ -4,8 +4,8 @@ "target": "@env(PREINIT)", "minVersion": "0.8", "compatibilityLevel": "JAVA_8", - "priority": -100000000, - "mixinPriority": -100000000, + "priority": 100000000, + "mixinPriority": 100000000, "required": true, "mixins": [ "AsmDecompilerMixin", From d84054fb605dbcfc181c3f3dc2c9ae20ffcdf446 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 4 Oct 2025 19:14:19 +0200 Subject: [PATCH 07/12] fix --- .../groovyscript/sandbox/AbstractGroovySandbox.java | 1 + .../cleanroommc/groovyscript/sandbox/MixinSandbox.java | 8 ++++---- .../groovyscript/sandbox/engine/ScriptEngine.java | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java index 0e922a942..e74b0b6b6 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java @@ -214,6 +214,7 @@ protected void load(Binding binding, Set executedClasses, boolean run) t } protected void loadScripts(Binding binding, Set executedClasses, boolean run) throws Throwable { + FileUtil.cleanScriptPathWarnedCache(); Collection files = getScriptFiles(); List scripts = this.engine.findScripts(files); for (CompiledScript compiledScript : scripts) { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index ed041ee83..a73b48c89 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -7,7 +7,6 @@ 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; @@ -153,8 +152,9 @@ protected void initConfig(CompilerConfiguration config) { LocalShortRef.class, Cancellable.class, Invoker.class, - Accessor.class, - WrapMethod.class) + Accessor.class + //WrapMethod.class + ) .stream() .map(Class::getName) .toArray(String[]::new)); @@ -201,7 +201,7 @@ private Collection collectCompiledMixins() { @Override public Collection getScriptFiles() { - return SandboxData.getSortedFilesOf(getScriptRoot(), Collections.singleton(SandboxData.MIXIN_PKG + "/")); + return SandboxData.getSortedFilesOf(getScriptRoot(), Collections.singleton(SandboxData.MIXIN_PKG + "/"), false); } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java index 85cd8a21d..d8ace15c2 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java @@ -21,6 +21,7 @@ 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; From deae6dea17207443309710fdeec36c78bbe21923 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 4 Oct 2025 20:01:09 +0200 Subject: [PATCH 08/12] implement friendlyhj's suggestions --- .../groovyscript/core/GroovyScriptCore.java | 6 +- .../core/LaunchClassLoaderResourceCache.java | 57 ++++++++++++++++++ .../groovyscript/core/MixinPlugin.java | 40 +++++++++++++ .../groovyscript/sandbox/MixinSandbox.java | 59 ++++++++----------- .../resources/mixin.groovyscript.custom.json | 1 + 5 files changed, 127 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java diff --git a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java index 6f2817d19..ef2f32c72 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java @@ -43,7 +43,11 @@ public void injectData(Map data) { source = (File) data.getOrDefault("coremodLocation", null); SandboxData.initialize((File) FMLInjectionData.data()[6], LOG); SideOnlyConfig.init(); - MixinSandbox.loadMixins(); + try { + MixinSandbox.loadMixins(); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override 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..324c24d64 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java @@ -0,0 +1,57 @@ +package com.cleanroommc.groovyscript.core; + +import com.google.common.collect.ForwardingMap; +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 probably don't need this :clueless: + 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/MixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java new file mode 100644 index 000000000..98b5d97e3 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java @@ -0,0 +1,40 @@ +package com.cleanroommc.groovyscript.core; + +import com.cleanroommc.groovyscript.sandbox.MixinSandbox; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +@ApiStatus.Internal +public class MixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String s) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String s, String s1) { + return true; + } + + @Override + public void acceptTargets(Set set, Set set1) {} + + @Override + public List getMixins() { + return MixinSandbox.getMixinClasses(); + } + + @Override + public void preApply(String s, org.objectweb.asm.tree.ClassNode classNode, String s1, IMixinInfo iMixinInfo) {} + + @Override + public void postApply(String s, org.objectweb.asm.tree.ClassNode classNode, String s1, IMixinInfo iMixinInfo) {} +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index a73b48c89..391fb2db9 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -1,6 +1,7 @@ package com.cleanroommc.groovyscript.sandbox; import com.cleanroommc.groovyscript.api.GroovyLog; +import com.cleanroommc.groovyscript.core.LaunchClassLoaderResourceCache; import com.cleanroommc.groovyscript.sandbox.engine.CompiledClass; import com.cleanroommc.groovyscript.sandbox.engine.CompiledScript; import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; @@ -15,8 +16,8 @@ import groovy.lang.Script; import groovyjarjarasm.asm.ClassVisitor; import groovyjarjarasm.asm.ClassWriter; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.launchwrapper.Launch; -import net.minecraft.launchwrapper.LaunchClassLoader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.codehaus.groovy.GroovyBugError; @@ -26,15 +27,14 @@ import org.codehaus.groovy.control.*; import org.codehaus.groovy.control.customizers.CompilationCustomizer; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; import org.spongepowered.asm.mixin.*; -import org.spongepowered.asm.mixin.extensibility.IMixinConfig; 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 org.spongepowered.asm.mixin.transformer.Config; import java.io.File; import java.lang.reflect.Field; @@ -42,14 +42,29 @@ public class MixinSandbox extends AbstractGroovySandbox { + private static MixinSandbox instance; + private static final Map injectedResourceCache = new Object2ObjectOpenHashMap<>(); + private static final List mixinClasses = new ArrayList<>(); + + @UnmodifiableView @ApiStatus.Internal - public static void loadMixins() { + public static List getMixinClasses() { + return Collections.unmodifiableList(mixinClasses); + } + + @ApiStatus.Internal + public static void loadMixins() throws Exception { if (instance != null) { throw new IllegalStateException("Mixins already loaded"); } instance = new MixinSandbox(); instance.run(LoadStage.MIXIN); + Field lclBytecodesField = Launch.classLoader.getClass().getDeclaredField("resourceCache"); + lclBytecodesField.setAccessible(true); + //noinspection unchecked + Map resourceCache = (Map) lclBytecodesField.get(Launch.classLoader); + // 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 = instance.collectCompiledMixins(); @@ -57,40 +72,14 @@ public static void loadMixins() { LOG.info("No groovy mixins configured"); return; } - String cfgName = "mixin.groovyscript.custom.json"; // create and register config + String cfgName = "mixin.groovyscript.custom.json"; Mixins.addConfiguration(cfgName); - // obtain the just created config - IMixinConfig config = Config.create(cfgName, MixinEnvironment.getDefaultEnvironment()).getConfig(); - List mixinClasses; - try { - Class mixinConfigClass = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); - Field field = mixinConfigClass.getDeclaredField("mixinClasses"); - field.setAccessible(true); - mixinClasses = (List) field.get(config); - } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - // inject loaded mixin classes into configuration - mixinClasses.addAll(groovyMixins); + lclBytecodesField.set(Launch.classLoader, new LaunchClassLoaderResourceCache(resourceCache, injectedResourceCache)); + mixinClasses.addAll(groovyMixins); // mixins are registered by a mixin config plugin at core.MixinPlugin instance.getEngine().writeIndex(); } - private static MixinSandbox instance; - private static final Map resourceCache; - - static { - Map resourceCache1; - try { - Field field = LaunchClassLoader.class.getDeclaredField("resourceCache"); - field.setAccessible(true); - resourceCache1 = (Map) field.get(Launch.classLoader); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - resourceCache = resourceCache1; - } - public static final Logger LOG = LogManager.getLogger("GroovyScript-MixinSandbox"); public static final boolean DEBUG = true; @@ -101,7 +90,7 @@ protected ScriptEngine createEngine(CompilerConfiguration config) { return new ScriptEngine(SandboxData.getRootUrls(), SandboxData.getMixinScriptCachePath(), SandboxData.getScriptFile(), config) .onClassLoaded(cc -> { GroovyLog.get().info(" - loaded mixin class {} from cache", cc.getName()); - resourceCache.put(cc.getName(), cc.getData()); + injectedResourceCache.put(cc.getName(), cc.getData()); }); } @@ -223,7 +212,7 @@ private ClassGenerator(SourceUnit su) { public void call(ClassVisitor classVisitor, ClassNode classNode) throws CompilationFailedException { // mixin will try to find the bytes in that map, so we will put them there byte[] code = ((ClassWriter) classVisitor).toByteArray(); - resourceCache.put(classNode.getName(), code); + injectedResourceCache.put(classNode.getName(), code); if (DEBUG) LOG.info("Generated groovy mixin class {}", classNode.getName()); MixinSandbox.this.getEngine().onCompileClass(this.su, classNode, null, code); } diff --git a/src/main/resources/mixin.groovyscript.custom.json b/src/main/resources/mixin.groovyscript.custom.json index a12d5e939..95c7a8000 100644 --- a/src/main/resources/mixin.groovyscript.custom.json +++ b/src/main/resources/mixin.groovyscript.custom.json @@ -1,6 +1,7 @@ { "package": "mixins", "refmap": "mixins.groovyscript.refmap.json", + "plugin": "com.cleanroommc.groovyscript.core.MixinPlugin", "target": "@env(DEFAULT)", "minVersion": "0.8", "compatibilityLevel": "JAVA_8", From 7a79b3b94bcdc709360e16a5c053cd2f18647aeb Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 4 Oct 2025 20:37:56 +0200 Subject: [PATCH 09/12] mod mixins are working --- dependencies.gradle | 1 + examples/mixins/TestGuisMixin.groovy | 17 +++++++++++++++++ .../core/LaunchClassLoaderResourceCache.java | 5 +++-- .../groovyscript/sandbox/MixinSandbox.java | 7 +++++-- 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 examples/mixins/TestGuisMixin.groovy diff --git a/dependencies.gradle b/dependencies.gradle index 6205ee975..2a22903d1 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -158,6 +158,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/mixins/TestGuisMixin.groovy b/examples/mixins/TestGuisMixin.groovy new file mode 100644 index 000000000..6beffc14b --- /dev/null +++ b/examples/mixins/TestGuisMixin.groovy @@ -0,0 +1,17 @@ +// X mods_loaded: modularui + +import com.cleanroommc.modularui.screen.ModularPanel; +import com.cleanroommc.modularui.screen.viewport.ModularGuiContext; +import com.cleanroommc.modularui.test.TestGuis; + +@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)); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java index 324c24d64..ed42596c4 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java @@ -1,6 +1,7 @@ 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; @@ -28,12 +29,12 @@ public class LaunchClassLoaderResourceCache extends ForwardingMap delegate; - // immutable to thread-safe // groovyscript: we probably don't need this :clueless: + // immutable to thread-safe // groovyscript: we make a copy instead of immutability private final Map injected; public LaunchClassLoaderResourceCache(Map delegate, Map injected) { this.delegate = delegate; - this.injected = injected; + this.injected = new Object2ObjectOpenHashMap<>(injected); } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index 391fb2db9..6fa92bb99 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -43,7 +43,7 @@ public class MixinSandbox extends AbstractGroovySandbox { private static MixinSandbox instance; - private static final Map injectedResourceCache = new Object2ObjectOpenHashMap<>(); + private static Map injectedResourceCache = new Object2ObjectOpenHashMap<>(); private static final List mixinClasses = new ArrayList<>(); @UnmodifiableView @@ -75,7 +75,9 @@ public static void loadMixins() throws Exception { // create and register config String cfgName = "mixin.groovyscript.custom.json"; Mixins.addConfiguration(cfgName); - lclBytecodesField.set(Launch.classLoader, new LaunchClassLoaderResourceCache(resourceCache, injectedResourceCache)); + // from now on we forbid modifying the map + MixinSandbox.injectedResourceCache = Collections.unmodifiableMap(MixinSandbox.injectedResourceCache); + lclBytecodesField.set(Launch.classLoader, new LaunchClassLoaderResourceCache(resourceCache, MixinSandbox.injectedResourceCache)); mixinClasses.addAll(groovyMixins); // mixins are registered by a mixin config plugin at core.MixinPlugin instance.getEngine().writeIndex(); } @@ -213,6 +215,7 @@ public void call(ClassVisitor classVisitor, ClassNode classNode) throws Compilat // mixin will try to find the bytes in that map, so we will put them there byte[] code = ((ClassWriter) classVisitor).toByteArray(); injectedResourceCache.put(classNode.getName(), code); + GroovyLog.get().info(" - compiled mixin class {}", classNode.getName()); if (DEBUG) LOG.info("Generated groovy mixin class {}", classNode.getName()); MixinSandbox.this.getEngine().onCompileClass(this.su, classNode, null, code); } From 49668fa1903ff0f7ebab2b0447f357760064781e Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 5 Oct 2025 09:49:51 +0200 Subject: [PATCH 10/12] verify mixin target --- .../cleanroommc/groovyscript/api/GroovyLog.java | 15 ++++++++++++++- .../groovyscript/core/MixinPlugin.java | 13 ++++++++++++- .../groovyscript/sandbox/GroovyLogImpl.java | 15 +++++++-------- .../sandbox/security/GroovySecurityManager.java | 6 +++++- 4 files changed, 38 insertions(+), 11 deletions(-) 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/core/MixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java index 98b5d97e3..ac9220c08 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java @@ -1,6 +1,9 @@ package com.cleanroommc.groovyscript.core; +import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.sandbox.MixinSandbox; +import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager; +import com.cleanroommc.groovyscript.sandbox.security.SandboxSecurityException; import org.jetbrains.annotations.ApiStatus; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; @@ -33,7 +36,15 @@ public List getMixins() { } @Override - public void preApply(String s, org.objectweb.asm.tree.ClassNode classNode, String s1, IMixinInfo iMixinInfo) {} + public void preApply(String className, org.objectweb.asm.tree.ClassNode classNode, String mixinName, IMixinInfo info) { + if (!GroovySecurityManager.INSTANCE.isValid(classNode, className)) { + GroovyLog.get() + .exception( + "An exception while applying a mixin occurred.", + new SandboxSecurityException("Can't mixin into class '" + className + "', since it is blacklisted for groovy!"), + true); + } + } @Override public void postApply(String s, org.objectweb.asm.tree.ClassNode classNode, String s1, IMixinInfo iMixinInfo) {} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java index e366bd3f8..fa4be9b35 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java @@ -261,14 +261,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:"; @@ -284,7 +278,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) { 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 4ea93127d..0ce1c9cb1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/GroovySecurityManager.java @@ -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) { From 07de72dca5c7430dd762bb6b14284a70afc785c9 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 6 Oct 2025 20:03:22 +0200 Subject: [PATCH 11/12] a looooooooooooooooooooot of stuff --- examples/lib/ui.groovy | 54 ++++++ examples/mixins/TestGuisMixin.groovy | 7 +- .../groovyscript/GroovyScript.java | 34 +--- .../core/AbstractMixinPlugin.java | 45 +++++ .../groovyscript/core/EarlyMixinPlugin.java | 15 ++ .../groovyscript/core/GroovyScriptCore.java | 2 +- .../groovyscript/core/LateMixin.java | 3 + .../groovyscript/core/LateMixinPlugin.java | 15 ++ .../core/LaunchClassLoaderResourceCache.java | 4 +- .../groovyscript/core/MixinPlugin.java | 51 ----- .../groovyscript/helper/PairList.java | 84 ++++++++ .../registry/ReloadableRegistryManager.java | 3 +- .../sandbox/AbstractGroovySandbox.java | 2 + .../groovyscript/sandbox/ActualSide.java | 53 +++++ .../groovyscript/sandbox/GroovyLogImpl.java | 5 +- .../groovyscript/sandbox/LoadStage.java | 5 +- .../groovyscript/sandbox/MixinSandbox.java | 182 ++++++++---------- .../groovyscript/sandbox/Preprocessor.java | 94 ++++++--- .../groovyscript/sandbox/RunConfig.java | 61 +++++- .../groovyscript/sandbox/SandboxData.java | 85 ++++++++ .../sandbox/engine/CompiledClass.java | 10 +- .../sandbox/engine/CompiledScript.java | 46 ++++- .../engine/GroovyScriptClassLoader.java | 52 ++--- .../sandbox/engine/MixinScriptEngine.java | 95 +++++++++ .../sandbox/engine/ScriptEngine.java | 71 ++++--- .../GroovyScriptMixinVerifier.java | 29 +++ ...n => mixin.groovyscript.custom.early.json} | 8 +- .../mixin.groovyscript.custom.late.json | 10 + 28 files changed, 820 insertions(+), 305 deletions(-) create mode 100644 examples/lib/ui.groovy create mode 100644 src/main/java/com/cleanroommc/groovyscript/core/AbstractMixinPlugin.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/core/EarlyMixinPlugin.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/core/LateMixinPlugin.java delete mode 100644 src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/helper/PairList.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/ActualSide.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptMixinVerifier.java rename src/main/resources/{mixin.groovyscript.custom.json => mixin.groovyscript.custom.early.json} (62%) create mode 100644 src/main/resources/mixin.groovyscript.custom.late.json 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/TestGuisMixin.groovy b/examples/mixins/TestGuisMixin.groovy index 6beffc14b..2c3c36119 100644 --- a/examples/mixins/TestGuisMixin.groovy +++ b/examples/mixins/TestGuisMixin.groovy @@ -1,8 +1,10 @@ -// X mods_loaded: modularui +// 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 { @@ -12,6 +14,7 @@ public abstract class TestGuisMixin { @Inject(method = "buildUI", at = @At("HEAD"), cancellable = true) public void buildUI(ModularGuiContext context, CallbackInfoReturnable cir) { - cir.setReturnValue(buildToggleGridListUI(context)); + //cir.setReturnValue(buildToggleGridListUI(context)); + cir.setReturnValue(ui.buildUI(context)); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index ec89318a9..9a93dd894 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -105,6 +105,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(); @@ -144,7 +145,6 @@ public void onRegisterItem(RegistryEvent.Register event) { @ApiStatus.Internal public static void initializeRunConfig(File minecraftHome) { SandboxData.initialize(minecraftHome, LOGGER); - reloadRunConfig(true); } @ApiStatus.Internal @@ -236,37 +236,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/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 ef2f32c72..407e0d996 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptCore.java @@ -44,7 +44,7 @@ public void injectData(Map data) { SandboxData.initialize((File) FMLInjectionData.data()[6], LOG); SideOnlyConfig.init(); try { - MixinSandbox.loadMixins(); + MixinSandbox.loadEarlyMixins(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/LateMixin.java index cab30d7e3..302789b84 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", @@ -48,6 +50,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 index ed42596c4..8607f34d5 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/LaunchClassLoaderResourceCache.java @@ -29,12 +29,12 @@ public class LaunchClassLoaderResourceCache extends ForwardingMap delegate; - // immutable to thread-safe // groovyscript: we make a copy instead of immutability + // 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 = new Object2ObjectOpenHashMap<>(injected); + this.injected = injected; } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java b/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java deleted file mode 100644 index ac9220c08..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/core/MixinPlugin.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.cleanroommc.groovyscript.core; - -import com.cleanroommc.groovyscript.api.GroovyLog; -import com.cleanroommc.groovyscript.sandbox.MixinSandbox; -import com.cleanroommc.groovyscript.sandbox.security.GroovySecurityManager; -import com.cleanroommc.groovyscript.sandbox.security.SandboxSecurityException; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; - -import java.util.List; -import java.util.Set; - -@ApiStatus.Internal -public class MixinPlugin implements IMixinConfigPlugin { - - @Override - public void onLoad(String s) {} - - @Override - public String getRefMapperConfig() { - return null; - } - - @Override - public boolean shouldApplyMixin(String s, String s1) { - return true; - } - - @Override - public void acceptTargets(Set set, Set set1) {} - - @Override - public List getMixins() { - return MixinSandbox.getMixinClasses(); - } - - @Override - public void preApply(String className, org.objectweb.asm.tree.ClassNode classNode, String mixinName, IMixinInfo info) { - if (!GroovySecurityManager.INSTANCE.isValid(classNode, className)) { - GroovyLog.get() - .exception( - "An exception while applying a mixin occurred.", - new SandboxSecurityException("Can't mixin into class '" + className + "', since it is blacklisted for groovy!"), - true); - } - } - - @Override - public void postApply(String s, org.objectweb.asm.tree.ClassNode classNode, String s1, IMixinInfo iMixinInfo) {} -} 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 index e74b0b6b6..b8ff1ecb0 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/AbstractGroovySandbox.java @@ -216,7 +216,9 @@ protected void load(Binding binding, Set executedClasses, boolean run) t 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); 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 fa4be9b35..aedd373b1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyLogImpl.java @@ -3,7 +3,6 @@ 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.FMLLaunchHandler; @@ -311,8 +310,8 @@ 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 - Side side = this.passedEarly ? FMLCommonHandler.instance().getEffectiveSide() : FMLLaunchHandler.side(); - return side.isClient() ? "CLIENT" : "SERVER"; + ActualSide side = SandboxData.getLogicalSide(); + return side.isPhysical() ? side.getName() : side.getShortName(); } private String getSource() { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java index 4c4fbefb2..1d4b87fcd 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/LoadStage.java @@ -6,7 +6,8 @@ public enum LoadStage { - MIXIN("mixin", false, -1000000000), + 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); @@ -44,7 +45,7 @@ public int getPriority() { } public boolean isMixin() { - return this == MIXIN; + return this == MIXIN_EARLY || this == MIXIN_LATE; } @Override diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java index 6fa92bb99..5e5c351d2 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/MixinSandbox.java @@ -2,30 +2,23 @@ import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.core.LaunchClassLoaderResourceCache; -import com.cleanroommc.groovyscript.sandbox.engine.CompiledClass; -import com.cleanroommc.groovyscript.sandbox.engine.CompiledScript; -import com.cleanroommc.groovyscript.sandbox.engine.ScriptEngine; +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 groovyjarjarasm.asm.ClassVisitor; -import groovyjarjarasm.asm.ClassWriter; 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.GroovyBugError; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.ModuleNode; -import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.*; -import org.codehaus.groovy.control.customizers.CompilationCustomizer; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.UnmodifiableView; import org.spongepowered.asm.mixin.*; @@ -44,55 +37,85 @@ public class MixinSandbox extends AbstractGroovySandbox { private static MixinSandbox instance; private static Map injectedResourceCache = new Object2ObjectOpenHashMap<>(); - private static final List mixinClasses = new ArrayList<>(); + private static final List earlyMixinClasses = new ArrayList<>(); + private static final List lateMixinClasses = new ArrayList<>(); @UnmodifiableView @ApiStatus.Internal - public static List getMixinClasses() { - return Collections.unmodifiableList(mixinClasses); + public static List getEarlyMixinClasses() { + return Collections.unmodifiableList(earlyMixinClasses); + } + + @UnmodifiableView + @ApiStatus.Internal + public static List getLateMixinClasses() { + return Collections.unmodifiableList(lateMixinClasses); } @ApiStatus.Internal - public static void loadMixins() throws Exception { - if (instance != null) { + public static void loadEarlyMixins() throws Exception { + if (MixinSandbox.instance != null) { throw new IllegalStateException("Mixins already loaded"); } - instance = new MixinSandbox(); - instance.run(LoadStage.MIXIN); + 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 = instance.collectCompiledMixins(); + Collection groovyMixins = MixinSandbox.instance.collectCompiledMixins(); if (groovyMixins.isEmpty()) { - LOG.info("No groovy mixins configured"); + LOG.info("No early groovy mixins configured"); return; } // create and register config - String cfgName = "mixin.groovyscript.custom.json"; - Mixins.addConfiguration(cfgName); + 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); - lclBytecodesField.set(Launch.classLoader, new LaunchClassLoaderResourceCache(resourceCache, MixinSandbox.injectedResourceCache)); - mixinClasses.addAll(groovyMixins); // mixins are registered by a mixin config plugin at core.MixinPlugin - instance.getEngine().writeIndex(); + + 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 ScriptEngine createEngine(CompilerConfiguration config) { - return new ScriptEngine(SandboxData.getRootUrls(), SandboxData.getMixinScriptCachePath(), SandboxData.getScriptFile(), config) + protected MixinScriptEngine createEngine(CompilerConfiguration config) { + return new MixinScriptEngine(SandboxData.getRootUrls(), SandboxData.getMixinScriptCachePath(), SandboxData.getScriptFile(), config) .onClassLoaded(cc -> { - GroovyLog.get().info(" - loaded mixin class {} from cache", cc.getName()); - injectedResourceCache.put(cc.getName(), cc.getData()); + 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); }); } @@ -143,14 +166,13 @@ protected void initConfig(CompilerConfiguration config) { LocalShortRef.class, Cancellable.class, Invoker.class, - Accessor.class - //WrapMethod.class + Accessor.class, + WrapMethod.class ) .stream() .map(Class::getName) .toArray(String[]::new)); super.initConfig(config); - config.addCompilationCustomizers(new CallbackInjector()); } @Override @@ -161,7 +183,7 @@ public boolean canRunInStage(LoadStage stage) { @Override protected void preRun() { if (ScriptEngine.DELETE_CACHE_ON_RUN) getEngine().deleteScriptCache(); - GroovyLog.get().infoMC("Running scripts in loader '{}'", getCurrentLoader()); + GroovyLog.get().infoMC("Loading mixin scripts in loader '{}'", getCurrentLoader()); } @Override @@ -177,16 +199,36 @@ 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<>(); - Iterator it = getEngine().classIterator(); - while (it.hasNext()) { - CompiledClass compiledClass = it.next(); - if (compiledClass.hasData()) { - if (DEBUG) LOG.info("found groovy mixin class {}", compiledClass.getName()); - mixinClasses.add(compiledClass.getName().substring(SandboxData.MIXIN_PKG.length() + 1)); - } + 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; } @@ -200,66 +242,6 @@ protected void loadScript(CompiledScript compiledScript, Binding binding, boolea long t = System.currentTimeMillis(); getEngine().loadScript(compiledScript); this.compileTime += System.currentTimeMillis() - t; - } - - private class ClassGenerator implements CompilationUnit.ClassgenCallback { - - private final SourceUnit su; - - private ClassGenerator(SourceUnit su) { - this.su = su; - } - - @Override - public void call(ClassVisitor classVisitor, ClassNode classNode) throws CompilationFailedException { - // mixin will try to find the bytes in that map, so we will put them there - byte[] code = ((ClassWriter) classVisitor).toByteArray(); - injectedResourceCache.put(classNode.getName(), code); - GroovyLog.get().info(" - compiled mixin class {}", classNode.getName()); - if (DEBUG) LOG.info("Generated groovy mixin class {}", classNode.getName()); - MixinSandbox.this.getEngine().onCompileClass(this.su, classNode, null, code); - } - } - - private class CallbackInjector extends CompilationCustomizer { - - public CallbackInjector() { - super(CompilePhase.CANONICALIZATION); - } - - public void call(CompilationUnit cu, SourceUnit su, ClassNode classNode) throws CompilationFailedException { - if (cu.getClassgenCallback() instanceof ClassGenerator) { - // this is a test to see if it is called multiple times on the same unit - GroovyLog.get().infoMC(" overwriting ClassGenerator with su {} and class {}", su.getName(), classNode.getName()); - } - cu.setClassgenCallback(new ClassGenerator(su)); - } - - private void changeBugText(final GroovyBugError e, final SourceUnit context, CompilationUnit unit) { - e.setBugText( - "exception in phase '" + unit.getPhaseDescription() + "' in source unit '" + (context != null - ? context.getName() - : "?") + "' " + e.getBugText()); - } - - @Override - public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException { - // we cant use this since we need the compile unit - throw new UnsupportedOperationException(); - } - - @Override - public void doPhaseOperation(final CompilationUnit unit) throws CompilationFailedException { - // is all this looping really needed? - for (ModuleNode module : unit.getAST().getModules()) { - for (ClassNode classNode : module.getClasses()) { - SourceUnit context = null; - context = classNode.getModule().getContext(); - if (context == null || context.getPhase() < unit.getPhase() || (context.getPhase() == unit.getPhase() && !context.isPhaseComplete())) { - call(unit, context, classNode); - } - } - } - } + 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 56781cae6..757149cce 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/RunConfig.java @@ -26,6 +26,16 @@ public class RunConfig { + public enum MixinSide { + CLIENT("client"), SERVER("server"), COMMON("common"); + + public final String name; + + MixinSide(String name) { + this.name = name; + } + } + public static JsonObject createDefaultJson() { JsonObject json = new JsonObject(); json.addProperty("packName", "PlaceHolder name"); @@ -61,20 +71,21 @@ 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; private final List packAuthors; private final Map> loaderPaths = new Object2ObjectOpenHashMap<>(); + //private final Map> mixinPaths = new Object2ObjectOpenHashMap<>(); private final List packmodeList = new ArrayList<>(); 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; @@ -120,6 +131,7 @@ public RunConfig(JsonObject json) { } else { this.packAuthors = Collections.emptyList(); } + //loadMixinPaths(json.get("mixins")); modMetadata.modId = this.packId; modMetadata.name = this.packName; modMetadata.version = this.version; @@ -129,9 +141,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,8 +148,6 @@ public void reload(JsonObject json, boolean init) { this.packmodePaths.clear(); this.packmodeConfigState = 0; - String regex = File.separatorChar == '\\' ? "/" : "\\\\"; - String replacement = getSeparator(); if (json.has("classes")) { throw new IllegalStateException("GroovyScript classes definition in runConfig is deprecated! Classes are now treated as normal scripts."); } @@ -158,7 +165,7 @@ public void reload(JsonObject json, boolean init) { List paths = new ArrayList<>(); for (JsonElement element : loader) { - String path = element.getAsString().replaceAll(regex, replacement); + String path = element.getAsString().replaceAll(separatorRegex, separatorReplacement); while (path.endsWith("/") || path.endsWith("\\")) { path = path.substring(0, path.length() - 1); } @@ -208,6 +215,41 @@ public void reload(JsonObject json, boolean init) { } } + /*private void loadMixinPaths(JsonElement mixinElement) { + if (mixinElement == null) { + mixinPaths.put(MixinSide.COMMON.name, Collections.singletonList("")); + return; + } + if (mixinElement.isJsonPrimitive()) { + mixinPaths.put(MixinSide.COMMON.name, Collections.singletonList(sanitizePath(mixinElement.getAsString()))); + return; + } + if (mixinElement.isJsonObject()) { + loadMixinSidePaths(MixinSide.CLIENT, mixinElement.getAsJsonObject()); + loadMixinSidePaths(MixinSide.SERVER, mixinElement.getAsJsonObject()); + loadMixinSidePaths(MixinSide.COMMON, mixinElement.getAsJsonObject()); + } + } + + private void loadMixinSidePaths(MixinSide side, JsonObject mixinJson) { + if (!mixinJson.has(side.name)) return; + JsonElement el = mixinJson.get(side.name); + if (el.isJsonPrimitive()) { + mixinPaths.put(side.name, Collections.singletonList(sanitizePath(el.getAsString()))); + } else if (el.isJsonArray()) { + List paths = new ArrayList<>(); + for (JsonElement path : el.getAsJsonArray()) { + if (path.isJsonPrimitive()) { + String s = sanitizePath(path.getAsString()); + if (!paths.contains(s)) { + paths.add(s); + } + } + } + mixinPaths.put(side.name, paths); + } + }*/ + public String getPackName() { return packName; } @@ -295,6 +337,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 49ed5bf0f..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,9 +18,11 @@ 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.nio.file.StandardOpenOption; import java.util.*; import java.util.stream.Stream; @@ -32,6 +41,8 @@ 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; @@ -42,13 +53,16 @@ public class SandboxData { 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(); @@ -85,6 +99,38 @@ public static void initialize(File minecraftHome, Logger log) { 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() { @@ -149,6 +195,11 @@ public static void initialize(File minecraftHome, Logger log) { return mixinRootUrls; } + public static RunConfig getRunConfig() { + ensureLoaded(); + return runConfig; + } + private static void ensureLoaded() { if (!initialised) { throw new IllegalStateException("Sandbox data is not yet Initialised."); @@ -159,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())); } @@ -167,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; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java index bc77c0b93..5bd91b0f1 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledClass.java @@ -1,6 +1,7 @@ package com.cleanroommc.groovyscript.sandbox.engine; 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; @@ -21,12 +22,17 @@ public class CompiledClass { final String name; byte[] data; Class clazz; - final boolean mixin; + boolean mixin; + boolean earlyMixin; public CompiledClass(String path, String name) { this.path = path; this.name = name; - this.mixin = name.startsWith("mixin"); + } + + public CompiledClass(String path, String name, boolean mixin) { + this(path, name); + this.mixin = mixin; } public void onCompile(byte @NotNull [] data, @Nullable Class clazz, String basePath) { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java index 7719679c5..c8788a43b 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/CompiledScript.java @@ -1,17 +1,20 @@ package com.cleanroommc.groovyscript.sandbox.engine; 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 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; @ApiStatus.Internal @@ -25,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; @@ -64,17 +68,28 @@ public CompiledClass findInnerClass(String clazz) { 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); } @@ -89,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; @@ -133,6 +156,7 @@ public boolean checkRequiresReload(File file, long lastModified, String rootPath // 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; } @@ -152,6 +176,10 @@ public boolean checkPreprocessorsFailed(File basePath) { return preprocessorCheckFailed(); } + public List getInnerClasses() { + return Collections.unmodifiableList(this.innerClasses); + } + public boolean requiresReload() { return this.requiresReload; } @@ -168,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/engine/GroovyScriptClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java index b3a05ae0a..ee7d6e723 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/GroovyScriptClassLoader.java @@ -281,19 +281,13 @@ protected GroovyClassLoader.ClassCollector createCollector(CompilationUnit unit, throw new UnsupportedOperationException(); } - public interface ClassgenCallback { - - void onGenerate(byte[] bytes, ClassNode classNode, Class clz); - } - 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 ClassgenCallback 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; @@ -306,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) { @@ -324,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.onGenerate(code, classNode, 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(ClassgenCallback 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..067ef5022 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java @@ -0,0 +1,95 @@ +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) { + 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; + } + + @Override + protected void onClassCompiled(CompiledClass cc, ClassNode classNode, byte[] bytecode, Class clz) { + super.onClassCompiled(cc, classNode, bytecode, clz); + } + + 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/engine/ScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java index d8ace15c2..bcbdb7029 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/ScriptEngine.java @@ -35,15 +35,15 @@ import java.net.URLConnection; import java.security.CodeSource; import java.util.*; -import java.util.function.Consumer; +@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,17 +72,20 @@ private static synchronized ThreadLocal getLocalData() { private final ScriptClassLoader classLoader; private final Map index = new Object2ObjectOpenHashMap<>(); private final Map loadedClasses = new Object2ObjectOpenHashMap<>(); - private Consumer onClassLoaded; 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(ScriptEngine.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; } @@ -99,9 +102,8 @@ public GroovyScriptClassLoader getClassLoader() { return classLoader; } - public ScriptEngine onClassLoaded(Consumer onClassLoaded) { - this.onClassLoaded = onClassLoaded; - return this; + protected Map getLoadedClasses() { + return loadedClasses; } public Iterator classIterator() { @@ -192,12 +194,12 @@ public 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); @@ -224,12 +226,16 @@ private void ensureClassLoaded(CompiledClass cc) { cc.clazz = classLoader.defineClass(cc.name, cc.data); } this.loadedClasses.put(cc.name, cc); - if (this.onClassLoaded != null) { - this.onClassLoaded.accept(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()); @@ -340,15 +346,11 @@ private File findScriptFile(String scriptName) { return clazz; } - /** - * Called via mixin when groovy compiled a class from scripts. - */ - @ApiStatus.Internal - public void onCompileClass(@NotNull SourceUnit su, @NotNull ClassNode classNode, @Nullable Class clazz, byte @NotNull [] code) { + 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 className = classNode.getName(); 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 @@ -359,8 +361,13 @@ public void onCompileClass(@NotNull SourceUnit su, @NotNull ClassNode classNode, CompiledScript comp = this.index.computeIfAbsent(truePath, k -> new CompiledScript(k, inner ? -1 : 0)); CompiledClass innerClass = comp; if (inner) innerClass = comp.findInnerClass(className); - innerClass.onCompile(code, clazz, this.cacheRoot.getPath()); - this.loadedClasses.put(innerClass.name, innerClass); + 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); } /** @@ -443,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); @@ -461,9 +483,7 @@ public URL loadResource(String name) throws MalformedURLException { @Override protected ClassCollector createCustomCollector(CompilationUnit unit, SourceUnit su) { - return super.createCustomCollector(unit, su).creatClassCallback((code, classNode, clz) -> { - onCompileClass(su, classNode, clz, code); - }); + return new ScriptEngine.ClassCollector(new InnerLoader(this), unit, su); } @Override @@ -505,6 +525,9 @@ public LookupResult findClassNode(String origName, CompilationUnit compilationUn 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); 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/resources/mixin.groovyscript.custom.json b/src/main/resources/mixin.groovyscript.custom.early.json similarity index 62% rename from src/main/resources/mixin.groovyscript.custom.json rename to src/main/resources/mixin.groovyscript.custom.early.json index 95c7a8000..a1f925d24 100644 --- a/src/main/resources/mixin.groovyscript.custom.json +++ b/src/main/resources/mixin.groovyscript.custom.early.json @@ -1,12 +1,10 @@ { "package": "mixins", "refmap": "mixins.groovyscript.refmap.json", - "plugin": "com.cleanroommc.groovyscript.core.MixinPlugin", + "plugin": "com.cleanroommc.groovyscript.core.EarlyMixinPlugin", "target": "@env(DEFAULT)", "minVersion": "0.8", "compatibilityLevel": "JAVA_8", - "mixins": [ - ], - "client": [ - ] + "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": [] +} From ad2982ab28e46baf5cf6efcde9f114cd80e5d2c6 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 6 Oct 2025 20:53:37 +0200 Subject: [PATCH 12/12] thing --- .../groovyscript/core/GroovyScriptTransformer.java | 3 --- .../groovyscript/sandbox/engine/MixinScriptEngine.java | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java index 447a78b1c..a47c3bd61 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/GroovyScriptTransformer.java @@ -19,9 +19,6 @@ public class GroovyScriptTransformer implements IClassTransformer { @Override public byte[] transform(String name, String transformedName, byte[] bytes) { if (bytes == null) return null; - if (transformedName.contains("PlayerLoggedInEvent") || transformedName.endsWith("PlayerList") || transformedName.endsWith("EntityPlayerMP") || transformedName.endsWith("NetworkManager") || transformedName.endsWith("NetHandlerPlayServer") || transformedName.endsWith("TextComponentString")) { - GroovyScriptCore.LOG.info("Loading class {}", transformedName); - } switch (name) { case InvokerHelperVisitor.CLASS_NAME: { ClassWriter classWriter = new ClassWriter(0); diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java index 067ef5022..00c4bde6a 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/engine/MixinScriptEngine.java @@ -40,6 +40,7 @@ public MixinScriptEngine onClassLoaded(Consumer onClassLoaded) { @Override protected void onClassLoaded(CompiledClass cc) { + super.onClassLoaded(cc); if (this.onClassLoaded != null) { this.onClassLoaded.accept(cc); } @@ -52,11 +53,6 @@ protected void onClassLoaded(CompiledClass cc) { return cc; } - @Override - protected void onClassCompiled(CompiledClass cc, ClassNode classNode, byte[] bytecode, Class clz) { - super.onClassCompiled(cc, classNode, bytecode, clz); - } - public static boolean isMixinClass(ClassNode classNode) { for (AnnotationNode annotationNode : classNode.getAnnotations()) { if (MIXIN_NODE.equals(annotationNode.getClassNode())) {