diff --git a/README.md b/README.md index 825545a..d4194ea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A programming game where the goal is to guide a robot through various levels using programmatic controls. ## Features -- Multi-language programming support (PHP, Python, JavaScript, Lua, and Google Blockly) +- Multi-language programming support (PHP, Python, JavaScript, Lua, Ruby, BASIC, and Google Blockly) - Custom code editor - Cloud Saves - Tutorial Levels @@ -31,26 +31,26 @@ A programming game where the goal is to guide a robot through various levels usi - [Chaquopy](https://chaquo.com/chaquopy) (Android Python) - [LuaJ](https://github.com/luaj/luaj) (Lua) - [Quercus](https://www.caucho.com/resin-3.1/doc/quercus.xtp) (PHP) -- [Duktape Android](https://github.com/cashapp/duktape-android) (Android JavaScript) -- [duktape4j](https://github.com/webfolderio/duktape4j) (Desktop JavaScript) +- [Duktape Android](https://github.com/TheLogicMaster/duktape-android) (Android JavaScript) +- [duktape4j](https://github.com/TheLogicMaster/duktape4j) (Desktop JavaScript) +- [Jasic](https://github.com/munificent/jasic) (BASIC interpreter base code) +- [JRuby](https://github.com/jruby/jruby) (Ruby) - [MaryTTS](https://github.com/marytts/marytts) (Desktop text to speech) - [Android MaryTTS](https://github.com/AndroidMaryTTS/AndroidMaryTTS) (Android text to speech) - [Project Lombok](https://projectlombok.org/) (Boilerplate generation) - [JCEF](https://github.com/chromiumembedded/java-cef) (Desktop embedded browser) - [JavaPackager](https://github.com/fvarrui/JavaPackager) (Gradle plugin to package desktop builds) -- [Jasic](https://github.com/munificent/jasic) (BASIC interpreter base code) -- [JRuby](https://github.com/jruby/jruby) (Ruby) ## Todo - Debug/step through mode -- Tab to double space functionality in editor +- Tab to double-space functionality in editor - Make the UI not terrible - Mobile HTML custom keyboard (Or not if it's not worth it and just disable code editor on mobile browsers) - Code testing (JS code transformations, for instance) - Level editor -- Trim HTML reflection cache of unused components like most of VisUI - Prevent Firefox '/' quick search functionality - Robot customization +- Android separate execution process using synchronous AIDL. Would provide a sandbox and the ability to interrupt infinite loops. ## Building ### GitHub Actions @@ -60,7 +60,7 @@ the web demo to the gh-pages branch, and the Android application to the Google P ### Building From Source Building the project requires several external libraries depending on which modules you want to build. Regardless of which platform you are building, the Android SDK is required to compile to project. To build without it, it would be -neccesary to comment out parts of `settings.gradle` and `build.gradle` at the project root. To enable debug mode +necessary to comment out parts of `settings.gradle` and `build.gradle` at the project root. To enable debug mode (mostly logging) for the entire project, add `debug=true` to a `local.properties` file at the root for the project. For the Desktop module, the respective [JCEF library](https://github.com/jcefbuild/jcefbuild/releases) for your platform is required at @@ -90,4 +90,4 @@ To build and copy Blockly, run the `updateBlockly` task. To update to a new JCEF release, download the platform assets from [jcefbuild](https://github.com/jcefbuild/jcefbuild/releases). The 6 native platform JARs in the release archives need to have their native libraries extracted to the `desktop/natives` directory with the existing format. The `desktop/libs` directory should have its contents updated from one of the new platform -release archives. The `Libraries/jcef` directory needs to be updated using the archive `bin/lib` directories. \ No newline at end of file +release archives. The `Libraries/jcef` directory needs to be updated using the archive `bin/lib` directories. diff --git a/android/build.gradle b/android/build.gradle index 5ae1ed8..0e10e60 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -37,12 +37,13 @@ android { exclude 'META-INF/dependencies.txt' // Contains outdated libpng library that prevents Play Store approval exclude 'lib/armeabi-v7a/libjniopencv_highgui.so' + exclude 'lib/armeabi-v7a/libopencv_highgui.so' } defaultConfig { applicationId 'com.thelogicmaster.robot_recharge' minSdkVersion 21 targetSdkVersion 29 - versionCode 9 + versionCode 10 versionName "1.0" multiDexEnabled true @@ -80,6 +81,7 @@ configurations { natives } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation fileTree(dir: 'lib', include: ['*.aar']) implementation project(':core') implementation project(':java') implementation "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion" @@ -93,7 +95,7 @@ dependencies { natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86" natives "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-x86_64" implementation 'org.apache.commons:commons-text:1.6' - implementation 'com.squareup.duktape:duktape-android:1.3.0' + //implementation 'com.squareup.duktape:duktape-android:1.3.0' implementation 'com.marytts.android:marylib:1.0.1' implementation "de.golfgl.gdxgamesvcs:gdx-gamesvcs-android-gpgs:$gamesvcsVersion" implementation "de.golfgl.gdxgamesvcs:gdx-gamesvcs-core-gamejolt:$gamesvcsVersion" diff --git a/android/lib/duktape.aar b/android/lib/duktape.aar new file mode 100644 index 0000000..e1e049b Binary files /dev/null and b/android/lib/duktape.aar differ diff --git a/android/src/main/java/com/thelogicmaster/robot_recharge/AndroidJavaScriptEngine.java b/android/src/main/java/com/thelogicmaster/robot_recharge/AndroidJavaScriptEngine.java index fab185b..8fad3ae 100644 --- a/android/src/main/java/com/thelogicmaster/robot_recharge/AndroidJavaScriptEngine.java +++ b/android/src/main/java/com/thelogicmaster/robot_recharge/AndroidJavaScriptEngine.java @@ -9,21 +9,32 @@ public class AndroidJavaScriptEngine implements CodeEngine { @Override public ExecutionInstance run(final IRobot robot, final String code, final ExecutionListener listener) { + Duktape duktape = Duktape.create(); Thread thread = new Thread(() -> { - try (Duktape duktape = Duktape.create()) { + try { duktape.set("Robot", IRobot.class, robot); duktape.evaluate(code); listener.onExecutionFinish(); } catch (Exception e) { - if (e instanceof InterruptedException) { + //noinspection ConstantConditions + if (e instanceof InterruptedException || + (e.getMessage() != null && e.getMessage().contains("RangeError: execution timeout"))) { listener.onExecutionInterrupted(); return; } Gdx.app.error("Duktape", e.toString()); listener.onExecutionError(e.getMessage()); + } finally { + duktape.close(); } }); thread.start(); - return new ExecutionInstance(thread); + return new ExecutionInstance(thread) { + @Override + public void stop () { + super.stop(); + duktape.interrupt(); + } + }; } } diff --git a/core/build.gradle b/core/build.gradle index 7885bf0..c25a12b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -15,12 +15,6 @@ dependencies { api "com.github.crykn.guacamole:core:$guacamoleVersion" api "com.github.crykn.guacamole:gdx:$guacamoleVersion" implementation "de.golfgl.gdxgamesvcs:gdx-gamesvcs-core:$gamesvcsVersion" - implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1' - implementation group: 'com.caucho', name: 'quercus', version: '4.0.45' - - // 9000 Versions have compatibility issues with Android, see: https://github.com/jruby/jruby/issues/4004 - //noinspection GradleDependency - implementation group: 'org.jruby', name: 'jruby-complete', version: '1.7.27' compileOnly 'org.projectlombok:lombok:1.18.16' annotationProcessor 'org.projectlombok:lombok:1.18.16' diff --git a/desktop/build.gradle b/desktop/build.gradle index 3cf303e..76aa267 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -23,7 +23,7 @@ dependencies { //implementation "com.github.crykn.guacamole:gdx:$guacamoleVersion" //implementation "com.github.crykn.guacamole:gdx-desktop:$guacamoleVersion" - implementation 'io.webfolder:duktape4j:1.2.1' + //implementation 'io.webfolder:duktape4j:1.2.1' implementation 'org.python:jython-slim:2.7.2' implementation group: 'de.dfki.mary', name: 'voice-cmu-slt-hsmm', version: '5.2' implementation "de.golfgl.gdxgamesvcs:gdx-gamesvcs-core-gamejolt:$gamesvcsVersion" diff --git a/desktop/libs/duktape4j-1.2.1.jar b/desktop/libs/duktape4j-1.2.1.jar new file mode 100644 index 0000000..7b00015 Binary files /dev/null and b/desktop/libs/duktape4j-1.2.1.jar differ diff --git a/desktop/src/main/java/com/thelogicmaster/robot_recharge/desktop/DesktopJavaScriptEngine.java b/desktop/src/main/java/com/thelogicmaster/robot_recharge/desktop/DesktopJavaScriptEngine.java index 16190d6..962de97 100644 --- a/desktop/src/main/java/com/thelogicmaster/robot_recharge/desktop/DesktopJavaScriptEngine.java +++ b/desktop/src/main/java/com/thelogicmaster/robot_recharge/desktop/DesktopJavaScriptEngine.java @@ -12,32 +12,31 @@ public class DesktopJavaScriptEngine implements CodeEngine { @Override public ExecutionInstance run(final IRobot robot, final String code, final ExecutionListener listener) { Duktape duktape = Duktape.create(); - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - duktape.set("Robot", IRobot.class, robot); - duktape.evaluate(code); - listener.onExecutionFinish(); - duktape.close(); - } catch (Exception e) { - duktape.close(); - if (e instanceof InterruptedException) { - listener.onExecutionInterrupted(); - return; - } - Gdx.app.error("Duktape", e.toString()); - listener.onExecutionError(e.getMessage()); + Thread thread = new Thread(() -> { + try { + duktape.set("Robot", IRobot.class, robot); + duktape.evaluate(code); + listener.onExecutionFinish(); + } catch (Exception e) { + //noinspection ConstantConditions + if (e instanceof InterruptedException || + (e.getMessage() != null && e.getMessage().contains("RangeError: execution timeout"))) { + listener.onExecutionInterrupted(); + return; } + Gdx.app.error("Duktape", e.toString()); + listener.onExecutionError(e.getMessage()); + } finally { + duktape.close(); } }); thread.start(); return new ExecutionInstance(thread) { @Override public void stop () { + thread.interrupt(); + duktape.interrupt(); super.stop(); - - duktape.close(); } }; } diff --git a/java/src/main/java/com/thelogicmaster/robot_recharge/ExecutionInstance.java b/java/src/main/java/com/thelogicmaster/robot_recharge/ExecutionInstance.java index 89c8b1b..868c0ec 100644 --- a/java/src/main/java/com/thelogicmaster/robot_recharge/ExecutionInstance.java +++ b/java/src/main/java/com/thelogicmaster/robot_recharge/ExecutionInstance.java @@ -1,5 +1,6 @@ package com.thelogicmaster.robot_recharge; +import com.badlogic.gdx.Application; import com.badlogic.gdx.Gdx; import com.thelogicmaster.robot_recharge.code.IExecutionInstance; import lombok.AllArgsConstructor; @@ -21,8 +22,9 @@ public void stop () { try { Thread.sleep(100); if (thread.isAlive()) { - thread.stop(); - Gdx.app.error("ExecutionInstance", "Had to kill thread, this is bad"); + if (Gdx.app.getType() != Application.ApplicationType.Android) + thread.stop(); + Gdx.app.error("ExecutionInstance", "Failed to interrupt thread, this is real bad"); } } catch (InterruptedException ignored) {} }).start(); diff --git a/java/src/main/java/com/thelogicmaster/robot_recharge/LuaEngine.java b/java/src/main/java/com/thelogicmaster/robot_recharge/LuaEngine.java index eb99963..ddf454b 100644 --- a/java/src/main/java/com/thelogicmaster/robot_recharge/LuaEngine.java +++ b/java/src/main/java/com/thelogicmaster/robot_recharge/LuaEngine.java @@ -37,7 +37,7 @@ public ExecutionInstance run(final IRobot robot, final String code, final Execut return new ExecutionInstance(thread); } - private class Watchdog extends DebugLib { + private static class Watchdog extends DebugLib { @Override @SneakyThrows diff --git a/java/src/main/java/com/thelogicmaster/robot_recharge/PhpEngine.java b/java/src/main/java/com/thelogicmaster/robot_recharge/PhpEngine.java index 0a8c0c2..b3d390d 100644 --- a/java/src/main/java/com/thelogicmaster/robot_recharge/PhpEngine.java +++ b/java/src/main/java/com/thelogicmaster/robot_recharge/PhpEngine.java @@ -20,10 +20,14 @@ public static IRobot getRobot() { public ExecutionInstance run(final IRobot robot, final String code, final ExecutionListener listener) { PhpEngine.robot = robot; Thread thread = new Thread(() -> { + QuercusEngine engine = new QuercusEngine(); try { - QuercusEngine engine = new QuercusEngine(); engine.init(); - engine.execute(""); + engine.execute(""); listener.onExecutionFinish(); } catch (QuercusException e) { if (e.getCause() instanceof InterruptedException) { @@ -35,6 +39,8 @@ public ExecutionInstance run(final IRobot robot, final String code, final Execut } catch (IOException e) { Gdx.app.error("PHP", "IO", e); listener.onExecutionError(e.getMessage()); + } finally { + engine.getQuercus().close(); } }); thread.start(); diff --git a/java/src/main/java/com/thelogicmaster/robot_recharge/RubyEngine.java b/java/src/main/java/com/thelogicmaster/robot_recharge/RubyEngine.java index ece8017..fffce9e 100644 --- a/java/src/main/java/com/thelogicmaster/robot_recharge/RubyEngine.java +++ b/java/src/main/java/com/thelogicmaster/robot_recharge/RubyEngine.java @@ -12,16 +12,15 @@ public class RubyEngine implements CodeEngine { @Override public ExecutionInstance run(final IRobot robot, final String code, final ExecutionListener listener) { - RubyInstanceConfig config = new RubyInstanceConfig(); - config.setCompileMode(RubyInstanceConfig.CompileMode.OFF); - Ruby ruby = Ruby.newInstance(config); Thread thread = new Thread(() -> { + RubyInstanceConfig config = new RubyInstanceConfig(); + config.setCompileMode(RubyInstanceConfig.CompileMode.OFF); + Ruby ruby = Ruby.newInstance(config); ruby.defineGlobalConstant("Robot", JavaUtil.convertJavaToRuby(ruby, robot)); try { ruby.evalScriptlet(code); JavaEmbedUtils.terminate(ruby); listener.onExecutionFinish(); - ruby.tearDown(); } catch (Exception e) { //noinspection ConstantConditions if (e instanceof InterruptedException) { @@ -30,17 +29,11 @@ public ExecutionInstance run(final IRobot robot, final String code, final Execut } Gdx.app.error("Ruby", e.toString()); listener.onExecutionError(e.getMessage()); + } finally { ruby.tearDown(); } }); thread.start(); - return new ExecutionInstance(thread) { - @Override - public void stop () { - super.stop(); - - ruby.tearDown(); - } - }; + return new ExecutionInstance(thread); } }