mirror of
https://github.com/Anuken/Mindustry.git
synced 2025-07-08 23:07:33 +07:00
Fixed scripts not working on older Android phones
This commit is contained in:
@ -28,7 +28,7 @@ dependencies{
|
||||
implementation project(":core")
|
||||
|
||||
implementation arcModule("backends:backend-android")
|
||||
implementation 'com.faendir.rhino:rhino-android:1.5.2'
|
||||
implementation 'com.jakewharton.android.repackaged:dalvik-dx:9.0.0_r3'
|
||||
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi"
|
||||
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
|
||||
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-arm64-v8a"
|
||||
|
@ -9,7 +9,6 @@ import android.os.Build.*;
|
||||
import android.os.*;
|
||||
import android.provider.Settings.*;
|
||||
import android.telephony.*;
|
||||
import com.faendir.rhino_android.*;
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.backends.android.surfaceview.*;
|
||||
import io.anuke.arc.files.*;
|
||||
@ -20,6 +19,7 @@ import io.anuke.arc.util.serialization.*;
|
||||
import io.anuke.mindustry.game.Saves.*;
|
||||
import io.anuke.mindustry.io.*;
|
||||
import io.anuke.mindustry.mod.*;
|
||||
import io.anuke.mindustry.rhino.*;
|
||||
import io.anuke.mindustry.ui.dialogs.*;
|
||||
|
||||
import java.io.*;
|
||||
@ -68,7 +68,7 @@ public class AndroidLauncher extends AndroidApplication{
|
||||
|
||||
@Override
|
||||
public org.mozilla.javascript.Context getScriptContext(){
|
||||
return new RhinoAndroidHelper(Core.files.local("script-output").file()).enterContext();
|
||||
return new RhinoBuilder(getContext()).enterContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,44 @@
|
||||
package io.anuke.mindustry.rhino;
|
||||
|
||||
import android.os.*;
|
||||
import org.mozilla.javascript.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Ensures that the classLoader used is correct
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
public class AndroidContextFactory extends ContextFactory{
|
||||
|
||||
private final File cacheDirectory;
|
||||
|
||||
/**
|
||||
* Create a new factory. It will cache generated code in the given directory
|
||||
* @param cacheDirectory the cache directory
|
||||
*/
|
||||
public AndroidContextFactory(File cacheDirectory){
|
||||
this.cacheDirectory = cacheDirectory;
|
||||
initApplicationClassLoader(createClassLoader(AndroidContextFactory.class.getClassLoader()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ClassLoader which is able to deal with bytecode
|
||||
* @param parent the parent of the create classloader
|
||||
* @return a new ClassLoader
|
||||
*/
|
||||
@Override
|
||||
public BaseAndroidClassLoader createClassLoader(ClassLoader parent){
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
|
||||
return new InMemoryAndroidClassLoader(parent);
|
||||
}
|
||||
return new FileAndroidClassLoader(parent, cacheDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onContextReleased(final Context cx){
|
||||
super.onContextReleased(cx);
|
||||
((BaseAndroidClassLoader)cx.getApplicationClassLoader()).reset();
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package io.anuke.mindustry.rhino;
|
||||
|
||||
import com.android.dex.*;
|
||||
import com.android.dx.cf.direct.*;
|
||||
import com.android.dx.command.dexer.*;
|
||||
import com.android.dx.dex.*;
|
||||
import com.android.dx.dex.cf.*;
|
||||
import com.android.dx.dex.file.*;
|
||||
import com.android.dx.merge.*;
|
||||
import org.mozilla.javascript.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Compiles java bytecode to dex bytecode and loads it
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
abstract class BaseAndroidClassLoader extends ClassLoader implements GeneratedClassLoader{
|
||||
|
||||
/**
|
||||
* Create a new instance with the given parent classloader
|
||||
* @param parent the parent
|
||||
*/
|
||||
public BaseAndroidClassLoader(ClassLoader parent){
|
||||
super(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Class<?> defineClass(String name, byte[] data){
|
||||
try{
|
||||
DexOptions dexOptions = new DexOptions();
|
||||
DexFile dexFile = new DexFile(dexOptions);
|
||||
DirectClassFile classFile = new DirectClassFile(data, name.replace('.', '/') + ".class", true);
|
||||
classFile.setAttributeFactory(StdAttributeFactory.THE_ONE);
|
||||
classFile.getMagic();
|
||||
DxContext context = new DxContext();
|
||||
dexFile.add(CfTranslator.translate(context, classFile, null, new CfOptions(), dexOptions, dexFile));
|
||||
Dex dex = new Dex(dexFile.toDex(null, false));
|
||||
Dex oldDex = getLastDex();
|
||||
if(oldDex != null){
|
||||
dex = new DexMerger(new Dex[]{dex, oldDex}, CollisionPolicy.KEEP_FIRST, context).merge();
|
||||
}
|
||||
return loadClass(dex, name);
|
||||
}catch(IOException | ClassNotFoundException e){
|
||||
throw new FatalLoadingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException;
|
||||
|
||||
protected abstract Dex getLastDex();
|
||||
|
||||
protected abstract void reset();
|
||||
|
||||
/**
|
||||
* Does nothing
|
||||
* @param aClass ignored
|
||||
*/
|
||||
@Override
|
||||
public void linkClass(Class<?> aClass){
|
||||
//doesn't make sense on android
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load a class. This will search all defined classes, all loaded jars and the parent class loader.
|
||||
* @param name the name of the class to load
|
||||
* @param resolve ignored
|
||||
* @return the class
|
||||
* @throws ClassNotFoundException if the class could not be found in any of the locations
|
||||
*/
|
||||
@Override
|
||||
public Class<?> loadClass(String name, boolean resolve)
|
||||
throws ClassNotFoundException{
|
||||
Class<?> loadedClass = findLoadedClass(name);
|
||||
if(loadedClass == null){
|
||||
Dex dex = getLastDex();
|
||||
if(dex != null){
|
||||
loadedClass = loadClass(dex, name);
|
||||
}
|
||||
if(loadedClass == null){
|
||||
loadedClass = getParent().loadClass(name);
|
||||
}
|
||||
}
|
||||
return loadedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Might be thrown in any Rhino method that loads bytecode if the loading failed
|
||||
*/
|
||||
public static class FatalLoadingException extends RuntimeException{
|
||||
FatalLoadingException(Throwable t){
|
||||
super("Failed to define class", t);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package io.anuke.mindustry.rhino;
|
||||
|
||||
import com.android.dex.*;
|
||||
import dalvik.system.*;
|
||||
import io.anuke.arc.*;
|
||||
import io.anuke.arc.backends.android.surfaceview.*;
|
||||
import io.anuke.arc.util.ArcAnnotate.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* @author F43nd1r
|
||||
* @since 24.10.2017
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
class FileAndroidClassLoader extends BaseAndroidClassLoader{
|
||||
private static int instanceCounter = 0;
|
||||
private final File dexFile;
|
||||
|
||||
/**
|
||||
* Create a new instance with the given parent classloader
|
||||
* @param parent the parent
|
||||
*/
|
||||
public FileAndroidClassLoader(ClassLoader parent, File cacheDir){
|
||||
super(parent);
|
||||
int id = instanceCounter++;
|
||||
dexFile = new File(cacheDir, id + ".dex");
|
||||
cacheDir.mkdirs();
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(@NonNull Dex dex, @NonNull String name) throws ClassNotFoundException{
|
||||
try{
|
||||
dex.writeTo(dexFile);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new DexClassLoader(dexFile.getPath(), ((AndroidApplication)Core.app).getContext().getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Dex getLastDex(){
|
||||
if(dexFile.exists()){
|
||||
try{
|
||||
return new Dex(dexFile);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset(){
|
||||
dexFile.delete();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package io.anuke.mindustry.rhino;
|
||||
|
||||
import android.annotation.*;
|
||||
import android.os.*;
|
||||
import com.android.dex.*;
|
||||
import dalvik.system.*;
|
||||
import io.anuke.arc.util.ArcAnnotate.NonNull;
|
||||
import io.anuke.arc.util.ArcAnnotate.Nullable;
|
||||
|
||||
import java.nio.*;
|
||||
|
||||
/**
|
||||
* @author F43nd1r
|
||||
* @since 24.10.2017
|
||||
*/
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
class InMemoryAndroidClassLoader extends BaseAndroidClassLoader{
|
||||
@Nullable
|
||||
private Dex last;
|
||||
|
||||
public InMemoryAndroidClassLoader(ClassLoader parent){
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(@NonNull Dex dex, @NonNull String name) throws ClassNotFoundException{
|
||||
last = dex;
|
||||
return new InMemoryDexClassLoader(ByteBuffer.wrap(dex.getBytes()), getParent()).loadClass(name);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Dex getLastDex(){
|
||||
return last;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset(){
|
||||
last = null;
|
||||
}
|
||||
}
|
83
android/src/io/anuke/mindustry/rhino/RhinoBuilder.java
Normal file
83
android/src/io/anuke/mindustry/rhino/RhinoBuilder.java
Normal file
@ -0,0 +1,83 @@
|
||||
package io.anuke.mindustry.rhino;
|
||||
|
||||
import org.mozilla.javascript.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Helps to prepare a Rhino Context for usage on android.
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
public class RhinoBuilder{
|
||||
private final File cacheDirectory;
|
||||
|
||||
/**
|
||||
* Constructs a new helper using the default temporary directory.
|
||||
* Note: It is recommended to use a custom directory, so no permission problems occur.
|
||||
*/
|
||||
public RhinoBuilder(){
|
||||
this(new File(System.getProperty("java.io.tmpdir", "."), "classes"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new helper using a directory in the applications cache.
|
||||
* @param context any context
|
||||
*/
|
||||
public RhinoBuilder(android.content.Context context){
|
||||
this(new File(context.getCacheDir(), "classes"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a helper using the specified directory as cache.
|
||||
* @param cacheDirectory the cache directory to use
|
||||
*/
|
||||
public RhinoBuilder(File cacheDirectory){
|
||||
this.cacheDirectory = cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* call this instead of {@link Context#enter()}
|
||||
* @return a context prepared for android
|
||||
*/
|
||||
public Context enterContext(){
|
||||
if(!SecurityController.hasGlobal())
|
||||
SecurityController.initGlobal(new SecurityController(){
|
||||
@Override
|
||||
public GeneratedClassLoader createClassLoader(ClassLoader classLoader, Object o){
|
||||
return Context.getCurrentContext().createClassLoader(classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDynamicSecurityDomain(Object o){
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return getContextFactory().enterContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The Context factory which has to be used on android.
|
||||
*/
|
||||
public AndroidContextFactory getContextFactory(){
|
||||
AndroidContextFactory factory;
|
||||
if(!ContextFactory.hasExplicitGlobal()){
|
||||
factory = new AndroidContextFactory(cacheDirectory);
|
||||
ContextFactory.getGlobalSetter().setContextFactoryGlobal(factory);
|
||||
}else if(!(ContextFactory.getGlobal() instanceof AndroidContextFactory)){
|
||||
throw new IllegalStateException("Cannot initialize factory for Android Rhino: There is already another factory");
|
||||
}else{
|
||||
factory = (AndroidContextFactory)ContextFactory.getGlobal();
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a context prepared for android
|
||||
* @deprecated use {@link #enterContext()} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public static Context prepareContext(){
|
||||
return new RhinoBuilder().enterContext();
|
||||
}
|
||||
}
|
@ -114,6 +114,7 @@ mod.author = [LIGHT_GRAY]Author:[] {0}
|
||||
mod.missing = This save contains mods that you have recently updated or no longer have installed. Save corruption may occur. Are you sure you want to load it?\n[lightgray]Mods:\n{0}
|
||||
mod.preview.missing = Before publishing this mod in the workshop, you must add an image preview.\nPlace an image named[accent] preview.png[] into the mod's folder and try again.
|
||||
mod.folder.missing = Only mods in folder form can be published on the workshop.\nTo convert any mod into a folder, simply unzip its file into a folder and delete the old zip, then restart your game or reload your mods.
|
||||
mod.scripts.unsupported = Your device does not support mod scripts. Some mods will not function correctly.
|
||||
|
||||
about.button = About
|
||||
name = Name:
|
||||
|
@ -381,6 +381,10 @@ public class Mods implements Loadable{
|
||||
requiresReload = false;
|
||||
|
||||
Events.fire(new ContentReloadEvent());
|
||||
|
||||
if(scripts != null && scripts.hasErrored()){
|
||||
Core.app.post(() -> Core.settings.getBoolOnce("scripts-errored", () -> ui.showErrorMessage("$mod.scripts.unsupported")));
|
||||
}
|
||||
}
|
||||
|
||||
/** This must be run on the main thread! */
|
||||
|
@ -12,20 +12,29 @@ public class Scripts implements Disposable{
|
||||
private final Context context;
|
||||
private final String wrapper;
|
||||
private Scriptable scope;
|
||||
private boolean errored;
|
||||
|
||||
public Scripts(){
|
||||
Time.mark();
|
||||
|
||||
context = Vars.platform.getScriptContext();
|
||||
context.setClassShutter(type -> (ClassAccess.allowedClassNames.contains(type) || type.startsWith("adapter") || type.contains("PrintStream") || type.startsWith("io.anuke.mindustry")) && !type.equals("io.anuke.mindustry.mod.ClassAccess"));
|
||||
context.setClassShutter(type -> (ClassAccess.allowedClassNames.contains(type) || type.startsWith("$Proxy") ||
|
||||
type.startsWith("adapter") || type.contains("PrintStream") ||
|
||||
type.startsWith("io.anuke.mindustry")) && !type.equals("io.anuke.mindustry.mod.ClassAccess"));
|
||||
|
||||
scope = new ImporterTopLevel(context);
|
||||
wrapper = Core.files.internal("scripts/wrapper.js").readString();
|
||||
|
||||
run(Core.files.internal("scripts/global.js").readString(), "global.js");
|
||||
if(!run(Core.files.internal("scripts/global.js").readString(), "global.js")){
|
||||
errored = true;
|
||||
}
|
||||
Log.debug("Time to load script engine: {0}", Time.elapsed());
|
||||
}
|
||||
|
||||
public boolean hasErrored(){
|
||||
return errored;
|
||||
}
|
||||
|
||||
public String runConsole(String text){
|
||||
try{
|
||||
Object o = context.evaluateString(scope, text, "console.js", 1, null);
|
||||
@ -58,11 +67,13 @@ public class Scripts implements Disposable{
|
||||
run(wrapper.replace("$SCRIPT_NAME$", mod.name + "/" + file.nameWithoutExtension()).replace("$CODE$", file.readString()).replace("$MOD_NAME$", mod.name), file.name());
|
||||
}
|
||||
|
||||
private void run(String script, String file){
|
||||
private boolean run(String script, String file){
|
||||
try{
|
||||
context.evaluateString(scope, script, file, 1, null);
|
||||
return true;
|
||||
}catch(Throwable t){
|
||||
log(LogLevel.err, file, "" + getError(t));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user