Fixed scripts not working on older Android phones

This commit is contained in:
Anuken
2019-12-09 12:48:15 -05:00
parent 177cea5c5d
commit 0078a8cb8e
10 changed files with 349 additions and 6 deletions

View File

@ -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"

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View 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();
}
}

View File

@ -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:

View File

@ -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! */

View File

@ -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;
}
}