diff --git a/build.gradle b/build.gradle
index a4436e1..12c6f8d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,8 @@
+import org.teavm.gradle.api.OptimizationLevel
+
plugins {
id "java"
+ id "org.teavm" version "0.10.2"
}
sourceSets {
@@ -22,10 +25,33 @@ tasks.withType(JavaCompile) {
dependencies {
implementation fileTree(dir: './lwjgl-rundir/', include: '*.jar')
+
+ teavm(teavm.libs.jso)
+ teavm(teavm.libs.jsoApis)
+ compileOnly "org.teavm:teavm-core:0.10.2" // workaround for a few hacks
}
+def folder = "javascript"
+def name = "classes.js"
+
+teavm.js {
+ obfuscated = true
+ sourceMap = true
+ targetFileName = "../" + name
+ optimization = OptimizationLevel.AGGRESSIVE
+ outOfProcess = false
+ fastGlobalAnalysis = false
+ processMemory = 512
+ entryPointName = "main"
+ mainClass = "net.lax1dude.eaglercraft.Client"
+ outputDir = file(folder)
+ properties = [ "java.util.TimeZone.autodetect": "true" ]
+ debugInformation = false
+}
+/*
tasks.register('copyDebugJar', Copy) {
project.delete("lwjgl-rundir/eaglercraft.jar")
from layout.buildDirectory.file("eaglercraft.jar")
into ("lwjgl-rundir/")
-}
\ No newline at end of file
+}
+*/
\ No newline at end of file
diff --git a/lwjgl-rundir/eaglercraft.jar b/lwjgl-rundir/eaglercraft.jar
index 0396202..cc119a5 100644
Binary files a/lwjgl-rundir/eaglercraft.jar and b/lwjgl-rundir/eaglercraft.jar differ
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/Client.java b/src/teavm/java/net/lax1dude/eaglercraft/Client.java
new file mode 100644
index 0000000..42ee668
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/Client.java
@@ -0,0 +1,391 @@
+package net.lax1dude.eaglercraft;
+
+import static org.teavm.jso.webgl.WebGLRenderingContext.*;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSExceptions;
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.browser.Window;
+import org.teavm.jso.core.JSError;
+import org.teavm.jso.dom.html.HTMLCanvasElement;
+import org.teavm.jso.dom.html.HTMLDocument;
+import org.teavm.jso.dom.html.HTMLElement;
+import org.teavm.jso.webgl.WebGLRenderingContext;
+
+import net.lax1dude.eaglercraft.adapter.DetectAnisotropicGlitch;
+import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2;
+import net.minecraft.src.Minecraft;
+import net.minecraft.src.ServerList;
+
+public class Client {
+ private static final String crashImage = "data:image/png;base64,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c+Jxky5I/Mh1xeM3zkWXBZ4pmpnUPLFILBU7WOlgVjI14mniiKrplC9kPVY5b3HWKjXWuid/YSivr6S5TmsEcSwhgSQkKKihjApsRGnXSbGQovOYj3/Y9SfJpZCrDEaOBVShQXb94H/we7ZWYWrSSwrFgO4Xx/kYBXp2gWbdcb6PHad5AgSfgSu97a82gNlP0uttLXIEDGwDF9dtTdkDLneA8JMhm7IrBWkJhQLwfkbflAMGb4G+NW9urXOcPgAZmtXyDXBwCIwVKXvd5929nXP7t6c1vx8743KRRjbQVgAADfdpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4KIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgIHhtbG5zOkdJTVA9Imh0dHA6Ly93d3cuZ2ltcC5vcmcveG1wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6NDJlMTU3MGEtNmMyZS00Y2E1LWI3ZTMtOGI4ODI1MmMwZDMwIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjU1NGY3N2UwLTc4NmEtNGFlZS1iYjhmLWNhYTBiZGNiYzE3MSIKICAgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmNmMWYyMjUxLWIwY2QtNDE1NS1hMjAyLTExNGI0ZGM2MmFhNSIKICAgZGM6Rm9ybWF0PSJpbWFnZS9wbmciCiAgIEdJTVA6QVBJPSIyLjAiCiAgIEdJTVA6UGxhdGZvcm09IldpbmRvd3MiCiAgIEdJTVA6VGltZVN0YW1wPSIxNjQzMDYxODUwNDk0OTc0IgogICBHSU1QOlZlcnNpb249IjIuMTAuMjQiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iCiAgICAgIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ODUyMGQ4YTMtMWRhZC00ZjIwLWFjOTktODg4OTJkZDExNDQ0IgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJHaW1wIDIuMTAgKFdpbmRvd3MpIgogICAgICBzdEV2dDp3aGVuPSIyMDIxLTEyLTE3VDE3OjIyOjQ4Ii8+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJkY2U5N2M4LTBkZjItNGQzNi1iMzE1LWE0YjdmMmUyMjJiNSIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChXaW5kb3dzKSIKICAgICAgc3RFdnQ6d2hlbj0iMjAyMi0wMS0yNFQxNDowNDoxMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz61xwk6AAAABmJLR0QAnQCdAJ2roJyEAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5gEYFgQKOBb3JwAAIABJREFUeNrtvXl0lFWePv7UvlelKvu+koSQRQiyBJGISEB0hFYZwW1sp4/2csaZ1jlz5sz80cc5c7rnaI8zju2o09qiIrKowEGURXYI+5IASQjZl0plT2rff3/073O/byVVlUQSRPs+5+QkkMpbb9333ud+lufzuaJgMBgEBwcHxw8UYj4EHBwcnMQ4ODg4OIlxcHBwcBLj4ODgJMbBwcHBSYyDg4ODkxgHBwcHJzEODg5OYhwcHBycxDg4ODg4iXFw/GWAqgHHfv+ufyv8+q7XvVMh4rWTHBwc3BLj4ODg4CTGwcHBwUmMg4ODkxgHBwcHJzEODg4OTmIcHBwcnMQ4ODg4iXFwcHBwEuPg4ODgJMbBwcHBSYyDg+NHBCkfAo6/ZAiLov1+P3w+H/x+P/x+P4LBIDQaDWQyGR+oSYyjSCSK+G9OYhwctwiHw4Guri44HA54vV74fD72FQgE4Pf74Xa74XA44Ha74Xa7EQgEsGrVKqSnp9+2BTlT5EJETT+PJZxgMAixWMx+FwgEIBb/2VGjn4W/o78P9/qxPSXo2pFez0mMg2MSC7mrqwt/+MMfYDabYbfb4XQ64XQ64XA42HeHw4HBwUFIJBJIJBLExMRg4cKFSEtL+0GSmJAwxsLn80EqlSIQCIRYomKxGD6fD2KxGH6/n31un88HiUQCv98fcm26Pr2H0LIlkhLeg/B3RIbCa30XS46TGMePHn6/HxaLBVu2bIFcLg9ZXLRQpFIpDAYDDAYDW0RyufwHRV7ChU9kQeQhtHyInLxeL7OwyFoS/o7caCI1+k7XlEgkjIiEJOXz+SASiSCVStl9SKX/j2pE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";
+
+ // avoid inlining of constant
+ private static String crashImageWrapper() {
+ return crashImage.substring(0);
+ }
+
+ @JSBody(params = { }, script = "if(window.eaglercraftOpts) { return (typeof window.eaglercraftOpts === \"string\") ? window.eaglercraftOpts :"
+ + "JSON.stringify(window.eaglercraftOpts); } else { return null; }")
+ private static native String getEaglerOpts();
+
+ public static HTMLElement rootElement = null;
+ public static Minecraft instance = null;
+ public static void main(String[] args) {
+ String newArgs = getEaglerOpts();
+ if(newArgs != null) {
+ crashScreenOptsDump = "window.eaglercraftOpts = " + newArgs;
+ try {
+ newMain(new JSONObject(newArgs));
+ }catch(JSONException ex) {
+ Window.alert("There's a JSON syntax error in window.eaglercraftOpts:\n" + ex.toString());
+ ex.printStackTrace();
+ return;
+ }
+ }else {
+ oldMain();
+ }
+ }
+
+ private static String crashScreenOptsDump = null;
+
+ private static void newMain(JSONObject conf) {
+
+ String containerEl = conf.getString("container");
+
+ rootElement = Window.current().getDocument().getElementById(containerEl);
+ if(rootElement == null) {
+ throw new JSONException("Container element \"" + containerEl + "\" does not exist in page");
+ }
+
+ EaglerAdapterImpl2.setServerToJoinOnLaunch(conf.optString("joinServer", null));
+
+ String assetsURI = conf.getString("assetsURI");
+ if(assetsURI.length() > 256) {
+ conf.put("assetsURI", assetsURI.substring(0, 256) + " ... ");
+ crashScreenOptsDump = "window.eaglercraftOpts = " + conf.toString();
+ }
+ String serverWorkerURI = conf.optString("serverWorkerURI", null);
+ EaglerAdapterImpl2.setWorldDatabaseName(conf.optString("worldsFolder", "MAIN"));
+
+ registerErrorHandler();
+
+ try {
+
+ EaglerAdapterImpl2.initializeContext(rootElement, assetsURI, serverWorkerURI);
+
+ ServerList.loadDefaultServers(conf);
+ AssetRepository.loadOverrides(conf);
+ LocalStorageManager.loadStorage();
+
+ run0();
+
+ }catch(Throwable t) {
+ showCrashScreen(t.toString() + "\n\n" + getStackTrace(t));
+ return;
+ }
+ }
+
+ private static void oldMain() {
+ String[] e = getOpts();
+ crashScreenOptsDump = "window.minecraftOpts = [ ";
+ for(int i = 0; i < e.length; ++i) {
+ String sh = e[i].length() > 512 ? (e[i].substring(0, 512) + "...") : e[i];
+ if(i > 0) {
+ crashScreenOptsDump += ", ";
+ }
+ crashScreenOptsDump += "\"" + sh + "\"";
+ }
+ crashScreenOptsDump += " ]";
+
+ registerErrorHandler();
+
+ try {
+
+ EaglerAdapterImpl2.initializeContext(rootElement = Window.current().getDocument().getElementById(e[0]), e[1], "worker_bootstrap.js");
+
+ LocalStorageManager.loadStorage();
+ if(e.length > 2 && e[2].length() > 0) {
+ ServerList.loadDefaultServers(e[2]);
+ }
+ if(e.length > 3) {
+ EaglerAdapterImpl2.setServerToJoinOnLaunch(e[3]);
+ }
+
+ run0();
+
+ }catch(Throwable t) {
+ showCrashScreen(t.toString() + "\n\n" + getStackTrace(t));
+ return;
+ }
+ }
+
+ private static String getStackTrace(Throwable t) {
+ JSObject obj = JSExceptions.getJSException(t);
+ if(obj != null) {
+ JSError err = (JSError)obj;
+ return err.getStack() == null ? "[no stack trace]" : err.getStack();
+ }else {
+ return "[no stack trace]";
+ }
+ }
+
+ private static void run0() {
+ System.out.println(" -------- starting minecraft -------- ");
+ instance = new Minecraft();
+ run1();
+ }
+
+ private static void run1() {
+ instance.run();
+ }
+
+ @JSBody(params = { }, script = "return window.minecraftOpts;")
+ public static native String[] getOpts();
+
+ public static void registerErrorHandler() {
+ setWindowErrorHandler(new WindowErrorHandler() {
+
+ @Override
+ public void call(String message, String file, int line, int col, JSError error) {
+ StringBuilder str = new StringBuilder();
+
+ str.append("Native Browser Exception\n");
+ str.append("----------------------------------\n");
+ str.append(" Line: ").append((file == null ? "unknown" : file) + ":" + line + ":" + col).append('\n');
+ str.append(" Type: ").append(error == null ? "generic" : error.getName()).append('\n');
+
+ if(error != null) {
+ str.append(" Desc: ").append(error.getMessage() == null ? "null" : error.getMessage()).append('\n');
+ }
+
+ if(message != null) {
+ if(error == null || error.getMessage() == null || !message.endsWith(error.getMessage())) {
+ str.append(" Desc: ").append(message).append('\n');
+ }
+ }
+
+ str.append("----------------------------------\n\n");
+ str.append(error.getStack() == null ? "No stack trace is available" : error.getStack()).append('\n');
+
+ showCrashScreen(str.toString());
+ }
+
+ });
+ }
+
+ @JSFunctor
+ private static interface WindowErrorHandler extends JSObject {
+ void call(String message, String file, int line, int col, JSError error);
+ }
+
+ @JSBody(params = { "handler" }, script = "window.addEventListener(\"error\", function(e) { handler("
+ + "(typeof e.message === \"string\") ? e.message : null,"
+ + "(typeof e.filename === \"string\") ? e.filename : null,"
+ + "(typeof e.lineno === \"number\") ? e.lineno : 0,"
+ + "(typeof e.colno === \"number\") ? e.colno : 0,"
+ + "(typeof e.error === \"undefined\") ? null : e.error); });")
+ public static native void setWindowErrorHandler(WindowErrorHandler handler);
+
+ private static boolean isCrashed = false;
+
+ private static void showCrashScreen(String t) {
+ if(!isCrashed) {
+ isCrashed = true;
+
+ StringBuilder str = new StringBuilder();
+ str.append("Game Crashed! I have fallen and I can't get up! If this has happened more than once then please copy the text on this screen and publish it in the issues feed of this fork's GitHub repository.\n\nThe URL to this fork's GitHub repository is: " + ConfigConstants.forkMe + "\n\n");
+ str.append(t);
+ str.append('\n').append('\n');
+ str.append("eaglercraft.version = \"").append(ConfigConstants.version).append("\"\n");
+ str.append("eaglercraft.minecraft = \"1.5.2\"\n");
+ str.append("eaglercraft.brand = \"lax1dude\"\n");
+ str.append("eaglercraft.username = \"").append(EaglerProfile.username).append("\"\n");
+ str.append('\n');
+ str.append(addWebGLToCrash());
+ str.append('\n');
+ str.append(crashScreenOptsDump).append('\n');
+ str.append('\n');
+ addDebugNav(str, "userAgent");
+ addDebugNav(str, "vendor");
+ addDebugNav(str, "language");
+ addDebugNav(str, "hardwareConcurrency");
+ addDebugNav(str, "deviceMemory");
+ addDebugNav(str, "platform");
+ addDebugNav(str, "product");
+ str.append('\n');
+ str.append("rootElement.clientWidth = ").append(rootElement.getClientWidth()).append('\n');
+ str.append("rootElement.clientHeight = ").append(rootElement.getClientHeight()).append('\n');
+ addDebug(str, "innerWidth");
+ addDebug(str, "innerHeight");
+ addDebug(str, "outerWidth");
+ addDebug(str, "outerHeight");
+ addDebug(str, "devicePixelRatio");
+ addDebugScreen(str, "availWidth");
+ addDebugScreen(str, "availHeight");
+ addDebugScreen(str, "colorDepth");
+ addDebugScreen(str, "pixelDepth");
+ str.append('\n');
+ addDebugLocation(str, "href");
+ str.append("\n----- Begin Minecraft Config -----\n");
+ str.append(LocalStorageManager.dumpConfiguration());
+ str.append("\n----- End Minecraft Config -----\n\n");
+ addDebug(str, "minecraftServer");
+
+ String s = rootElement.getAttribute("style");
+ rootElement.setAttribute("style", (s == null ? "" : s) + "position:relative;");
+ HTMLDocument doc = Window.current().getDocument();
+ HTMLElement img = doc.createElement("img");
+ HTMLElement div = doc.createElement("div");
+ img.setAttribute("style", "z-index:100;position:absolute;top:10px;left:calc(50% - 151px);");
+ img.setAttribute("src", crashImageWrapper());
+ div.setAttribute("style", "z-index:100;position:absolute;top:135px;left:10%;right:10%;bottom:30px;background-color:white;border:1px solid #cccccc;overflow-x:hidden;overflow-y:scroll;overflow-wrap:break-word;white-space:pre-wrap;font: 14px monospace;padding:10px;");
+ rootElement.appendChild(img);
+ rootElement.appendChild(div);
+ div.appendChild(doc.createTextNode(str.toString()));
+
+ EaglerAdapterImpl2.removeEventHandlers();
+
+ }
+ }
+
+ private static String addWebGLToCrash() {
+ StringBuilder ret = new StringBuilder();
+
+ WebGLRenderingContext ctx = EaglerAdapterImpl2.webgl;
+
+ if(ctx == null) {
+ HTMLCanvasElement cvs = (HTMLCanvasElement) Window.current().getDocument().createElement("canvas");
+
+ cvs.setWidth(64);
+ cvs.setHeight(64);
+
+ ctx = (WebGLRenderingContext)cvs.getContext("webgl");
+ }
+
+ if(ctx != null) {
+ if(EaglerAdapterImpl2.webgl != null) {
+ ret.append("webgl.version = ").append(ctx.getParameterString(VERSION)).append('\n');
+ }
+ if(ctx.getExtension("WEBGL_debug_renderer_info") != null) {
+ ret.append("webgl.renderer = ").append(ctx.getParameterString(/* UNMASKED_RENDERER_WEBGL */ 0x9246)).append('\n');
+ ret.append("webgl.vendor = ").append(ctx.getParameterString(/* UNMASKED_VENDOR_WEBGL */ 0x9245)).append('\n');
+ }else {
+ ret.append("webgl.renderer = ").append("" + ctx.getParameterString(RENDERER) + " [masked]").append('\n');
+ ret.append("webgl.vendor = ").append("" + ctx.getParameterString(VENDOR) + " [masked]").append('\n');
+ }
+ ret.append("\nwebgl.anisotropicGlitch = ").append(DetectAnisotropicGlitch.hasGlitch()).append('\n');
+ }else {
+ ret.append("Failed to query GPU info!\n");
+ }
+
+ return ret.toString();
+ }
+
+ public static void showIncompatibleScreen(String t) {
+ if(!isCrashed) {
+ isCrashed = true;
+
+ String s = rootElement.getAttribute("style");
+ rootElement.setAttribute("style", (s == null ? "" : s) + "position:relative;");
+ HTMLDocument doc = Window.current().getDocument();
+ HTMLElement img = doc.createElement("img");
+ HTMLElement div = doc.createElement("div");
+ img.setAttribute("style", "z-index:100;position:absolute;top:10px;left:calc(50% - 151px);");
+ img.setAttribute("src", crashImageWrapper());
+ div.setAttribute("style", "z-index:100;position:absolute;top:135px;left:10%;right:10%;bottom:30px;background-color:white;border:1px solid #cccccc;overflow-x:hidden;overflow-y:scroll;font:18px sans-serif;padding:40px;");
+ rootElement.appendChild(img);
+ rootElement.appendChild(div);
+ div.setInnerHTML("
+ This device is incompatible with Eaglercraft :(
"
+ + ""
+ + "
Issue:
"
+ + "
"
+ + "
"
+ + "
Things you can try:
"
+ + "
"
+ + "- Just try using Eaglercraft on a different device, it isn't a bug it's common sense
"
+ + "- If you are on a mobile device, please try a proper desktop or a laptop computer
"
+ + "- If you are using a device with no mouse cursor, please use a device with a mouse cursor
"
+ + "- If you are not using Chrome/Edge, try installing the latest Google Chrome
"
+ + "- If your browser is out of date, please update it to the latest version
"
+ + "- If you are using an old OS such as Windows 7, please try Windows 10 or 11
"
+ + "- If you have a GPU launched before 2009, WebGL 2.0 support may be impossible
"
+ + "
"
+ + "
");
+
+ div.querySelector("#crashReason").appendChild(doc.createTextNode(t));
+ div.querySelector("#crashUserAgent").appendChild(doc.createTextNode(getStringNav("userAgent")));
+
+ EaglerAdapterImpl2.removeEventHandlers();
+
+ String webGLRenderer = "No GL_RENDERER string could be queried";
+
+ try {
+ HTMLCanvasElement cvs = (HTMLCanvasElement) Window.current().getDocument().createElement("canvas");
+
+ cvs.setWidth(64);
+ cvs.setHeight(64);
+
+ WebGLRenderingContext ctx = (WebGLRenderingContext)cvs.getContext("webgl");
+
+ if(ctx != null) {
+ String r;
+ if(ctx.getExtension("WEBGL_debug_renderer_info") != null) {
+ r = ctx.getParameterString(/* UNMASKED_RENDERER_WEBGL */ 0x9246);
+ }else {
+ r = ctx.getParameterString(RENDERER);
+ if(r != null) {
+ r += " [masked]";
+ }
+ }
+ if(r != null) {
+ webGLRenderer = r;
+ }
+ }
+ }catch(Throwable tt) {
+ }
+
+ div.querySelector("#crashWebGL").appendChild(doc.createTextNode(webGLRenderer));
+
+ }
+ }
+
+ @JSBody(params = { "v" }, script = "try { return \"\"+window[v]; } catch(e) { return \"\"; }")
+ private static native String getString(String var);
+
+ @JSBody(params = { "v" }, script = "try { return \"\"+window.navigator[v]; } catch(e) { return \"\"; }")
+ private static native String getStringNav(String var);
+
+ @JSBody(params = { "v" }, script = "try { return \"\"+window.screen[v]; } catch(e) { return \"\"; }")
+ private static native String getStringScreen(String var);
+
+ @JSBody(params = { "v" }, script = "try { return \"\"+window.location[v]; } catch(e) { return \"\"; }")
+ private static native String getStringLocation(String var);
+
+ private static void addDebug(StringBuilder str, String var) {
+ str.append("window.").append(var).append(" = ").append(getString(var)).append('\n');
+ }
+
+ private static void addDebugNav(StringBuilder str, String var) {
+ str.append("window.navigator.").append(var).append(" = ").append(getStringNav(var)).append('\n');
+ }
+
+ private static void addDebugScreen(StringBuilder str, String var) {
+ str.append("window.screen.").append(var).append(" = ").append(getStringScreen(var)).append('\n');
+ }
+
+ private static void addDebugLocation(StringBuilder str, String var) {
+ str.append("window.location.").append(var).append(" = ").append(getStringLocation(var)).append('\n');
+ }
+
+ private static void addArray(StringBuilder str, String var) {
+ str.append("window.").append(var).append(" = ").append(getArray(var)).append('\n');
+ }
+
+ @JSBody(params = { "v" }, script = "try { return (typeof window[v] !== \"undefined\") ? JSON.stringify(window[v]) : \"[\\\"\\\"]\"; } catch(e) { return \"[\\\"\\\"]\"; }")
+ private static native String getArray(String var);
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/DetectAnisotropicGlitch.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/DetectAnisotropicGlitch.java
new file mode 100644
index 0000000..3f5c968
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/DetectAnisotropicGlitch.java
@@ -0,0 +1,189 @@
+package net.lax1dude.eaglercraft.adapter;
+
+import static net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext.*;
+
+import org.teavm.jso.browser.Window;
+import org.teavm.jso.dom.html.HTMLCanvasElement;
+import org.teavm.jso.typedarrays.Uint8Array;
+
+import net.lax1dude.eaglercraft.Client;
+import net.lax1dude.eaglercraft.adapter.teavm.TeaVMUtils;
+import net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext;
+import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray;
+
+import org.teavm.jso.webgl.*;
+
+public class DetectAnisotropicGlitch {
+
+ private static boolean known = false;
+ private static boolean detected = false;
+
+ public static boolean hasGlitch() {
+ if(!known) {
+ detected = detect();
+ known = true;
+ }
+ return detected;
+ }
+
+ public static boolean detect() {
+ HTMLCanvasElement cvs = (HTMLCanvasElement) Window.current().getDocument().createElement("canvas");
+
+ cvs.setWidth(400);
+ cvs.setHeight(300);
+
+ WebGL2RenderingContext ctx = (WebGL2RenderingContext) cvs.getContext("webgl2");
+
+ if(ctx == null) {
+ Client.showIncompatibleScreen("WebGL 2.0 is not supported on this device!");
+ throw new UnsupportedOperationException("WebGL 2 is not supported on this device!");
+ }
+
+ if(ctx.getExtension("EXT_texture_filter_anisotropic") != null) {
+
+ String vshSrc = "#version 300 es\n"
+ + "precision lowp float;"
+ + "in vec2 a_pos;"
+ + "out vec2 v_pos;"
+ + "void main() {"
+ + " gl_Position = vec4((v_pos = a_pos) * 2.0 - 1.0, 0.0, 1.0);"
+ + "}";
+
+ String fshSrc = "#version 300 es\n"
+ + "precision lowp float;"
+ + "precision lowp sampler2D;"
+ + "uniform sampler2D tex;"
+ + "in vec2 v_pos;"
+ + "out vec4 fragColor;"
+ + "void main() {"
+ + " fragColor = vec4(texture(tex, v_pos).rgb, 1.0);"
+ + "}";
+
+ WebGLShader vsh = ctx.createShader(VERTEX_SHADER);
+ ctx.shaderSource(vsh, vshSrc);
+ ctx.compileShader(vsh);
+
+ if(!ctx.getShaderParameterb(vsh, COMPILE_STATUS)) {
+ System.err.println("ERROR: Could not check for ANGLE Issue #4994, VERTEX_SHADER did not compile:");
+ System.err.println(ctx.getShaderInfoLog(vsh));
+ ctx.deleteShader(vsh);
+ return false;
+ }
+
+ WebGLShader fsh = ctx.createShader(FRAGMENT_SHADER);
+ ctx.shaderSource(fsh, fshSrc);
+ ctx.compileShader(fsh);
+
+ if(!ctx.getShaderParameterb(fsh, COMPILE_STATUS)) {
+ System.err.println("ERROR: Could not check for ANGLE Issue #4994, FRAGMENT_SHADER did not compile:");
+ System.err.println(ctx.getShaderInfoLog(fsh));
+ ctx.deleteShader(vsh);
+ ctx.deleteShader(fsh);
+ return false;
+ }
+
+ WebGLProgram pr = ctx.createProgram();
+
+ ctx.attachShader(pr, vsh);
+ ctx.attachShader(pr, fsh);
+
+ ctx.bindAttribLocation(pr, 0, "a_pos");
+ ctx.bindAttribLocation(pr, 0, "fragColor");
+
+ ctx.linkProgram(pr);
+
+ ctx.detachShader(pr, vsh);
+ ctx.detachShader(pr, fsh);
+
+ ctx.deleteShader(vsh);
+ ctx.deleteShader(fsh);
+
+ if(!ctx.getProgramParameterb(pr, LINK_STATUS)) {
+ System.err.println("ERROR: Could not check for ANGLE Issue #4994, program did not link:");
+ System.err.println(ctx.getProgramInfoLog(pr));
+ ctx.deleteProgram(pr);
+ return false;
+ }
+
+ ctx.useProgram(pr);
+
+ ctx.uniform1i(ctx.getUniformLocation(pr, "tex"), 0);
+
+ byte x0 = (byte)0x00;
+ byte x1 = (byte)0xFF;
+
+ byte[] pixelsData = new byte[] {
+ x0, x0, x0, x1,
+ x0, x0, x0, x1,
+ x1, x1, x1, x1,
+ x0, x0, x0, x1,
+ x0, x0, x0, x1,
+ x0, x0, x0, x1,
+ x1, x1, x1, x1,
+ x0, x0, x0, x1,
+ x0, x0, x0, x1,
+ x0, x0, x0, x1,
+ x1, x1, x1, x1,
+ x0, x0, x0, x1
+ };
+
+ WebGLTexture tex = ctx.createTexture();
+
+ ctx.bindTexture(TEXTURE_2D, tex);
+
+ ctx.texParameteri(TEXTURE_2D, TEXTURE_WRAP_S, REPEAT);
+ ctx.texParameteri(TEXTURE_2D, TEXTURE_WRAP_T, REPEAT);
+ ctx.texParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST_MIPMAP_LINEAR);
+ ctx.texParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST);
+ ctx.texParameterf(TEXTURE_2D, TEXTURE_MAX_ANISOTROPY_EXT, 16.0f);
+
+ ctx.texImage2D(TEXTURE_2D, 0, RGBA, 4, 3, 0, RGBA, UNSIGNED_BYTE, TeaVMUtils.unwrapUnsignedByteArray(pixelsData));
+ ctx.generateMipmap(TEXTURE_2D);
+
+ float[] vertsData = new float[] {
+ 0.0f, 0.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 1.0f, 1.0f,
+ 0.0f, 1.0f
+ };
+
+ WebGLBuffer buf = ctx.createBuffer();
+
+ ctx.bindBuffer(ARRAY_BUFFER, buf);
+ ctx.bufferData(ARRAY_BUFFER, TeaVMUtils.unwrapFloatArray(vertsData), STATIC_DRAW);
+
+ WebGLVertexArray arr = ctx.createVertexArray();
+
+ ctx.bindVertexArray(arr);
+
+ ctx.enableVertexAttribArray(0);
+ ctx.vertexAttribPointer(0, 2, FLOAT, false, 8, 0);
+
+ ctx.viewport(0, 0, 400, 300);
+ ctx.drawArrays(TRIANGLES, 0, 6);
+
+ ctx.deleteVertexArray(arr);
+ ctx.deleteBuffer(buf);
+ ctx.deleteTexture(tex);
+ ctx.deleteProgram(pr);
+
+ Uint8Array readPx = new Uint8Array(4);
+ ctx.readPixels(175, 150, 1, 1, RGBA, UNSIGNED_BYTE, readPx);
+
+ boolean b = (readPx.get(0) + readPx.get(1) + readPx.get(2)) != 0;
+
+ if(b) {
+ System.out.println("ANGLE issue #4994 is unpatched on this browser, enabling anisotropic fix");
+ }
+
+ return b;
+ }else {
+ System.err.println("WARNING: EXT_texture_filter_anisotropic is not supported!");
+ return false;
+ }
+
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java
new file mode 100644
index 0000000..b50f0f8
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java
@@ -0,0 +1,4222 @@
+package net.lax1dude.eaglercraft.adapter;
+
+import static net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.json.JSONObject;
+import org.teavm.interop.Async;
+import org.teavm.interop.AsyncCallback;
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.JSProperty;
+import org.teavm.jso.ajax.ReadyStateChangeHandler;
+import org.teavm.jso.ajax.XMLHttpRequest;
+import org.teavm.jso.browser.Storage;
+import org.teavm.jso.browser.TimerHandler;
+import org.teavm.jso.browser.Window;
+import org.teavm.jso.canvas.CanvasRenderingContext2D;
+import org.teavm.jso.canvas.ImageData;
+import org.teavm.jso.core.JSString;
+import org.teavm.jso.dom.css.CSSStyleDeclaration;
+import org.teavm.jso.dom.events.ErrorEvent;
+import org.teavm.jso.dom.events.Event;
+import org.teavm.jso.dom.events.EventListener;
+import org.teavm.jso.dom.events.KeyboardEvent;
+import org.teavm.jso.dom.events.MessageEvent;
+import org.teavm.jso.dom.events.MouseEvent;
+import org.teavm.jso.dom.events.WheelEvent;
+import org.teavm.jso.dom.html.HTMLAudioElement;
+import org.teavm.jso.dom.html.HTMLCanvasElement;
+import org.teavm.jso.dom.html.HTMLDocument;
+import org.teavm.jso.dom.html.HTMLElement;
+import org.teavm.jso.dom.html.HTMLImageElement;
+import org.teavm.jso.dom.html.HTMLVideoElement;
+import org.teavm.jso.media.MediaError;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+import org.teavm.jso.typedarrays.DataView;
+import org.teavm.jso.typedarrays.Int32Array;
+import org.teavm.jso.typedarrays.Uint8Array;
+import org.teavm.jso.typedarrays.Uint8ClampedArray;
+import org.teavm.jso.webaudio.AnalyserNode;
+import org.teavm.jso.webaudio.AudioBuffer;
+import org.teavm.jso.webaudio.AudioBufferSourceNode;
+import org.teavm.jso.webaudio.AudioContext;
+import org.teavm.jso.webaudio.AudioListener;
+import org.teavm.jso.webaudio.AudioNode;
+import org.teavm.jso.webaudio.ChannelMergerNode;
+import org.teavm.jso.webaudio.DecodeErrorCallback;
+import org.teavm.jso.webaudio.DecodeSuccessCallback;
+import org.teavm.jso.webaudio.GainNode;
+import org.teavm.jso.webaudio.MediaElementAudioSourceNode;
+import org.teavm.jso.webaudio.MediaEvent;
+import org.teavm.jso.webaudio.MediaStream;
+import org.teavm.jso.webaudio.MediaStreamAudioSourceNode;
+import org.teavm.jso.webaudio.PannerNode;
+import org.teavm.jso.webgl.WebGLBuffer;
+import org.teavm.jso.webgl.WebGLFramebuffer;
+import org.teavm.jso.webgl.WebGLProgram;
+import org.teavm.jso.webgl.WebGLRenderbuffer;
+import org.teavm.jso.webgl.WebGLShader;
+import org.teavm.jso.webgl.WebGLTexture;
+import org.teavm.jso.webgl.WebGLUniformLocation;
+import org.teavm.jso.websocket.CloseEvent;
+import org.teavm.jso.websocket.WebSocket;
+import org.teavm.jso.workers.Worker;
+import org.teavm.platform.Platform;
+import org.teavm.platform.PlatformRunnable;
+
+import net.lax1dude.eaglercraft.AssetRepository;
+import net.lax1dude.eaglercraft.Base64;
+import net.lax1dude.eaglercraft.Client;
+import net.lax1dude.eaglercraft.EaglerAdapter;
+import net.lax1dude.eaglercraft.EaglerImage;
+import net.lax1dude.eaglercraft.EaglerInputStream;
+import net.lax1dude.eaglercraft.EaglerProfile;
+import net.lax1dude.eaglercraft.EarlyLoadScreen;
+import net.lax1dude.eaglercraft.ExpiringSet;
+import net.lax1dude.eaglercraft.IntegratedServer;
+import net.lax1dude.eaglercraft.LANPeerEvent;
+import net.lax1dude.eaglercraft.LocalStorageManager;
+import net.lax1dude.eaglercraft.PKT;
+import net.lax1dude.eaglercraft.RelayQuery;
+import net.lax1dude.eaglercraft.RelayServerSocket;
+import net.lax1dude.eaglercraft.RelayWorldsQuery;
+import net.lax1dude.eaglercraft.ServerQuery;
+import net.lax1dude.eaglercraft.adapter.teavm.BufferConverter;
+import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANClient;
+import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANServer;
+import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftVoiceClient;
+import net.lax1dude.eaglercraft.adapter.teavm.MessageChannel;
+import net.lax1dude.eaglercraft.adapter.teavm.SelfDefence;
+import net.lax1dude.eaglercraft.adapter.teavm.TeaVMUtils;
+import net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext;
+import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery;
+import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray;
+import net.lax1dude.eaglercraft.glemu.EaglerAdapterGL30;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket00Handshake;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket69Pong;
+import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFFErrorCode;
+import net.minecraft.src.MathHelper;
+
+public class EaglerAdapterImpl2 {
+
+ public static final boolean _wisWebGL() {
+ return true;
+ }
+ public static final String _wgetShaderHeader() {
+ return "#version 300 es";
+ }
+
+ @JSBody(params = { }, script = "return window.location.href;")
+ private static native String getLocationString();
+
+ public static final boolean isSSLPage() {
+ return getLocationString().startsWith("https");
+ }
+
+ public static final InputStream loadResource(String path) {
+ byte[] file = loadResourceBytes(path);
+ if (file != null) {
+ return new EaglerInputStream(file);
+ } else {
+ return null;
+ }
+ }
+
+ public static final byte[] loadResourceBytes(String path) {
+ return AssetRepository.getResource(path);
+ }
+
+ public static final String fileContents(String path) {
+ byte[] contents = loadResourceBytes(path);
+ if(contents == null) {
+ return null;
+ }else {
+ return new String(contents, Charset.forName("UTF-8"));
+ }
+ }
+
+ public static final String[] fileContentsLines(String path) {
+ String contents = fileContents(path);
+ if(contents == null) {
+ return null;
+ }else {
+ return contents.replace("\r\n", "\n").split("[\r\n]");
+ }
+ }
+
+ @Async
+ public static native String downloadAssetPack(String assetPackageURI);
+
+ private static void downloadAssetPack(String assetPackageURI, final AsyncCallback cb) {
+ final XMLHttpRequest request = new XMLHttpRequest();
+ request.setResponseType("arraybuffer");
+ request.open("GET", assetPackageURI, true);
+ request.setOnReadyStateChange(new ReadyStateChangeHandler() {
+ @Override
+ public void stateChanged() {
+ if(request.getReadyState() == XMLHttpRequest.DONE) {
+ loadedPackage = TeaVMUtils.wrapByteArrayBuffer((ArrayBuffer)request.getResponse());
+ cb.complete("yee");
+ }
+ }
+ });
+ request.send();
+ }
+
+ @Async
+ public static native byte[] downloadURL(String url);
+
+ private static void downloadURL(String url, final AsyncCallback cb) {
+ if(url.isEmpty()) {
+ cb.complete(new byte[0]);
+ return;
+ }
+ final XMLHttpRequest request = new XMLHttpRequest();
+ request.setResponseType("arraybuffer");
+ request.open("GET", url, true);
+ request.setOnReadyStateChange(new ReadyStateChangeHandler() {
+ @Override
+ public void stateChanged() {
+ if(request.getReadyState() == XMLHttpRequest.DONE) {
+ cb.complete(TeaVMUtils.wrapByteArrayBuffer((ArrayBuffer)request.getResponse()));
+ }
+ }
+ });
+ request.send();
+ }
+
+ @JSBody(params = { "v", "s" }, script = "window[v] = s;")
+ public static native void setDebugVar(String v, String s);
+
+ @JSBody(params = { }, script = "if(window.navigator.userActivation){return window.navigator.userActivation.hasBeenActive;}else{return false;}")
+ public static native boolean hasBeenActive();
+
+ public static HTMLDocument doc = null;
+ public static HTMLElement parent = null;
+ public static HTMLCanvasElement canvas = null;
+ public static WebGL2RenderingContext webgl = null;
+ public static FramebufferGL backBuffer = null;
+ public static RenderbufferGL backBufferColor = null;
+ public static RenderbufferGL backBufferDepth = null;
+ public static Window win = null;
+ private static byte[] loadedPackage = null;
+ private static EventListener> contextmenu = null;
+ private static EventListener> mousedown = null;
+ private static EventListener> mouseup = null;
+ private static EventListener> mousemove = null;
+ private static EventListener> keydown = null;
+ private static EventListener> keyup = null;
+ private static EventListener> keypress = null;
+ private static EventListener> wheel = null;
+ private static String[] identifier = new String[0];
+ private static String integratedServerScript = "worker_bootstrap.js";
+ private static boolean anisotropicFilteringSupported = false;
+ private static boolean vsyncSupport = false;
+ private static int vsyncTimeout = -1;
+ private static boolean useDelayOnSwap = false;
+ private static MessageChannel immediateContinueChannel = null;
+ private static Runnable currentMsgChannelContinueHack = null;
+
+ private static final EagsFileChooser fileChooser = initFileChooser();
+
+ public static final String[] getIdentifier() {
+ return identifier;
+ }
+
+ @JSBody(params = { "v" }, script = "try { return \"\"+window.navigator[v]; } catch(e) { return \"\"; }")
+ private static native String getNavString(String var);
+
+ public static void onWindowUnload() {
+ LocalStorageManager.saveStorageA();
+ LocalStorageManager.saveStorageG();
+ LocalStorageManager.saveStorageP();
+ }
+
+ @JSBody(params = { "m" }, script = "return m.offsetX;")
+ private static native int getOffsetX(MouseEvent m);
+
+ @JSBody(params = { "m" }, script = "return m.offsetY;")
+ private static native int getOffsetY(MouseEvent m);
+
+ @JSBody(params = { "e" }, script = "return e.which;")
+ private static native int getWhich(KeyboardEvent e);
+
+ public static final void initializeContext(HTMLElement rootElement, String assetPackageURI, String serverWorkerURI) {
+ parent = rootElement;
+ String s = parent.getAttribute("style");
+ parent.setAttribute("style", (s == null ? "" : s)+"overflow-x:hidden;overflow-y:hidden;");
+ win = Window.current();
+ doc = win.getDocument();
+ integratedServerScript = serverWorkerURI;
+ double r = win.getDevicePixelRatio();
+ int iw = parent.getClientWidth();
+ int ih = parent.getClientHeight();
+ int sw = (int)(r * iw);
+ int sh = (int)(r * ih);
+ canvas = (HTMLCanvasElement)doc.createElement("canvas");
+ CSSStyleDeclaration canvasStyle = canvas.getStyle();
+ canvasStyle.setProperty("width", "100%");
+ canvasStyle.setProperty("height", "100%");
+ canvasStyle.setProperty("image-rendering", "pixelated");
+ canvas.setWidth(sw);
+ canvas.setHeight(sh);
+ rootElement.appendChild(canvas);
+ try {
+ doc.exitPointerLock();
+ }catch(Throwable t) {
+ Client.showIncompatibleScreen("Mouse cursor lock is not available on this device!");
+ throw new RuntimeException("Mouse cursor lock is not available on this device!");
+ }
+ SelfDefence.init(canvas);
+ webgl = (WebGL2RenderingContext) canvas.getContext("webgl2", youEagler());
+ if(webgl == null) {
+ Client.showIncompatibleScreen("WebGL 2.0 is not supported on this device!");
+ throw new RuntimeException("WebGL 2.0 is not supported in your browser ("+getNavString("userAgent")+")");
+ }
+
+ setupBackBuffer();
+ resizeBackBuffer(sw, sh);
+
+ anisotropicFilteringSupported = webgl.getExtension("EXT_texture_filter_anisotropic") != null;
+
+ win.addEventListener("contextmenu", contextmenu = new EventListener() {
+ @Override
+ public void handleEvent(MouseEvent evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ canvas.addEventListener("mousedown", mousedown = new EventListener() {
+ @Override
+ public void handleEvent(MouseEvent evt) {
+ int b = evt.getButton();
+ buttonStates[b == 1 ? 2 : (b == 2 ? 1 : b)] = true;
+ mouseEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ canvas.addEventListener("mouseup", mouseup = new EventListener() {
+ @Override
+ public void handleEvent(MouseEvent evt) {
+ int b = evt.getButton();
+ buttonStates[b == 1 ? 2 : (b == 2 ? 1 : b)] = false;
+ mouseEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ canvas.addEventListener("mousemove", mousemove = new EventListener() {
+ @Override
+ public void handleEvent(MouseEvent evt) {
+ mouseX = (int)(getOffsetX(evt) * win.getDevicePixelRatio());
+ mouseY = (int)((canvas.getClientHeight() - getOffsetY(evt)) * win.getDevicePixelRatio());
+ mouseDX += evt.getMovementX();
+ mouseDY += -evt.getMovementY();
+ if(hasBeenActive()) {
+ mouseEvents.add(evt);
+ }
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ win.addEventListener("keydown", keydown = new EventListener() {
+ @Override
+ public void handleEvent(KeyboardEvent evt) {
+ //keyStates[remapKey(evt.getKeyCode())] = true;
+ keyStates[remapKey(getWhich(evt))] = true;
+ keyEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ win.addEventListener("keyup", keyup = new EventListener() {
+ @Override
+ public void handleEvent(KeyboardEvent evt) {
+ //keyStates[remapKey(evt.getKeyCode())] = false;
+ keyStates[remapKey(getWhich(evt))] = false;
+ keyEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ win.addEventListener("keypress", keypress = new EventListener() {
+ @Override
+ public void handleEvent(KeyboardEvent evt) {
+ if(enableRepeatEvents && evt.isRepeat()) keyEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ canvas.addEventListener("wheel", wheel = new EventListener() {
+ @Override
+ public void handleEvent(WheelEvent evt) {
+ mouseEvents.add(evt);
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ });
+ win.addEventListener("blur", new EventListener() {
+ @Override
+ public void handleEvent(WheelEvent evt) {
+ isWindowFocused = false;
+ }
+ });
+ win.addEventListener("focus", new EventListener() {
+ @Override
+ public void handleEvent(WheelEvent evt) {
+ isWindowFocused = true;
+ }
+ });
+ onBeforeCloseRegister();
+
+ checkImmediateContinueSupport();
+
+ vsyncTimeout = -1;
+ vsyncSupport = false;
+
+ try {
+ asyncRequestAnimationFrame();
+ vsyncSupport = true;
+ }catch(Throwable t) {
+ System.err.println("VSync is not supported on this browser!");
+ }
+
+ initFileChooser();
+
+ EarlyLoadScreen.paintScreen();
+
+ voiceClient = startVoiceClient();
+ rtcLANClient = startRTCLANClient();
+
+ //todo: safely skip startRTCLANServer() if the integrated server is disabled:
+
+ //if(integratedServerScript != null) {
+ rtcLANServer = startRTCLANServer();
+ //}
+
+ downloadAssetPack(assetPackageURI);
+
+ try {
+ AssetRepository.install(loadedPackage);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ if(mouseEvents.isEmpty() && keyEvents.isEmpty() && !hasBeenActive()) {
+ EarlyLoadScreen.paintEnable();
+
+ while(mouseEvents.isEmpty() && keyEvents.isEmpty()) {
+ sleep(100);
+ }
+ }
+
+ audioctx = new AudioContext();
+ masterVolumeNode = audioctx.createGain();
+ masterVolumeNode.getGain().setValue(1.0f);
+ masterVolumeNode.connect(audioctx.getDestination());
+ musicVolumeNode = audioctx.createGain();
+ musicVolumeNode.getGain().setValue(1.0f);
+ musicVolumeNode.connect(audioctx.getDestination());
+
+ mouseEvents.clear();
+ keyEvents.clear();
+
+ Window.setInterval(new TimerHandler() {
+ @Override
+ public void onTimer() {
+ if(!videosBuffer.isEmpty()) {
+ long now = steadyTimeMillis();
+ Iterator vids = videosBuffer.values().iterator();
+ while(vids.hasNext()) {
+ BufferedVideo v = vids.next();
+ if(now - v.requestedTime > v.ttl) {
+ v.videoElement.setSrc("");
+ vids.remove();
+ }
+ }
+ }
+ }
+ }, 5000);
+ }
+
+ @JSBody(params = { }, script = "return window.startVoiceClient();")
+ private static native EaglercraftVoiceClient startVoiceClient();
+
+ private static interface EagsFileChooser extends JSObject {
+
+ @JSProperty
+ HTMLElement getInputElement();
+
+ void openFileChooser(String ext, String mime);
+
+ @JSProperty
+ ArrayBuffer getFileChooserResult();
+
+ @JSProperty
+ void setFileChooserResult(ArrayBuffer val);
+
+ @JSProperty
+ String getFileChooserResultName();
+
+ @JSProperty
+ void setFileChooserResultName(String str);
+
+ }
+
+ @JSBody(params = { }, script =
+ "var ret = {\r\n" +
+ "inputElement: null,\r\n" +
+ "openFileChooser: function(ext, mime){\r\n" +
+ "var el = ret.inputElement = document.createElement(\"input\");\r\n" +
+ "el.type = \"file\";\r\n" +
+ "el.multiple = false;\r\n" +
+ "el.addEventListener(\"change\", function(evt){\r\n" +
+ "var f = ret.inputElement.files;\r\n" +
+ "if(f.length == 0){\r\n" +
+ "ret.fileChooserResult = null;\r\n" +
+ "}else{\r\n" +
+ "ret.fileChooserResult = null;\r\n" +
+ "ret.fileChooserResultName = null;\r\n" +
+ "var phile = f[0];\r\n" +
+ "phile.arrayBuffer().then(function(res) {\r\n" +
+ "ret.fileChooserResult = res;\r\n" +
+ "ret.fileChooserResultName = phile.name;console.log(ret);\r\n" +
+ "});\r\n" +
+ "}\r\n" +
+ "});\r\n" +
+ "el.accept = \".\" + ext;\r\n" +
+ "el.click();\r\n" +
+ "},\r\n" +
+ "fileChooserResult: null,\r\n" +
+ "fileChooserResultName: null\r\n" +
+ "}; return ret;")
+ private static native EagsFileChooser initFileChooser();
+
+ public static final void destroyContext() {
+
+ }
+
+ public static final void removeEventHandlers() {
+ try {
+ win.removeEventListener("contextmenu", contextmenu);
+ canvas.removeEventListener("mousedown", mousedown);
+ canvas.removeEventListener("mouseup", mouseup);
+ canvas.removeEventListener("mousemove", mousemove);
+ win.removeEventListener("keydown", keydown);
+ win.removeEventListener("keyup", keyup);
+ win.removeEventListener("keypress", keypress);
+ canvas.removeEventListener("wheel", wheel);
+ }catch(Throwable t) {
+ }
+ try {
+ String screenImg = canvas.toDataURL("image/png");
+ canvas.delete();
+ HTMLImageElement newImage = (HTMLImageElement) doc.createElement("img");
+ newImage.setSrc(screenImg);
+ newImage.setWidth(parent.getClientWidth());
+ newImage.setHeight(parent.getClientHeight());
+ parent.appendChild(newImage);
+ }catch(Throwable t) {
+ }
+ }
+
+ private static LinkedList mouseEvents = new LinkedList<>();
+ private static LinkedList keyEvents = new LinkedList<>();
+
+ private static int mouseX = 0;
+ private static int mouseY = 0;
+ private static double mouseDX = 0.0D;
+ private static double mouseDY = 0.0D;
+ private static int width = 0;
+ private static int height = 0;
+ private static boolean enableRepeatEvents = false;
+ private static boolean isWindowFocused = true;
+
+ @JSBody(params = { }, script = "return {antialias: false, depth: true, powerPreference: \"high-performance\", desynchronized: true, preserveDrawingBuffer: false, premultipliedAlpha: false, alpha: false};")
+ public static native JSObject youEagler();
+
+ @JSBody(params = { }, script = "return { willReadFrequently: true };")
+ public static native JSObject youEagler2();
+
+ public static final int _wGL_TEXTURE_2D = TEXTURE_2D;
+ public static final int _wGL_DEPTH_TEST = DEPTH_TEST;
+ public static final int _wGL_LEQUAL = LEQUAL;
+ public static final int _wGL_GEQUAL = GEQUAL;
+ public static final int _wGL_GREATER = GREATER;
+ public static final int _wGL_LESS = LESS;
+ public static final int _wGL_BACK = BACK;
+ public static final int _wGL_FRONT = FRONT;
+ public static final int _wGL_FRONT_AND_BACK = FRONT_AND_BACK;
+ public static final int _wGL_COLOR_BUFFER_BIT = COLOR_BUFFER_BIT;
+ public static final int _wGL_DEPTH_BUFFER_BIT = DEPTH_BUFFER_BIT;
+ public static final int _wGL_BLEND = BLEND;
+ public static final int _wGL_RGBA = RGBA;
+ public static final int _wGL_RGB = RGB;
+ public static final int _wGL_RGB8 = RGB8;
+ public static final int _wGL_RGBA8 = RGBA8;
+ public static final int _wGL_RED = RED;
+ public static final int _wGL_R8 = R8;
+ public static final int _wGL_UNSIGNED_BYTE = UNSIGNED_BYTE;
+ public static final int _wGL_UNSIGNED_SHORT = UNSIGNED_SHORT;
+ public static final int _wGL_SRC_ALPHA = SRC_ALPHA;
+ public static final int _wGL_ONE_MINUS_SRC_ALPHA = ONE_MINUS_SRC_ALPHA;
+ public static final int _wGL_ONE_MINUS_DST_COLOR = ONE_MINUS_DST_COLOR;
+ public static final int _wGL_ONE_MINUS_SRC_COLOR = ONE_MINUS_SRC_COLOR;
+ public static final int _wGL_ZERO = ZERO;
+ public static final int _wGL_CULL_FACE = CULL_FACE;
+ public static final int _wGL_TEXTURE_MIN_FILTER = TEXTURE_MIN_FILTER;
+ public static final int _wGL_TEXTURE_MAG_FILTER = TEXTURE_MAG_FILTER;
+ public static final int _wGL_LINEAR = LINEAR;
+ public static final int _wGL_EQUAL = EQUAL;
+ public static final int _wGL_SRC_COLOR = SRC_COLOR;
+ public static final int _wGL_ONE = ONE;
+ public static final int _wGL_NEAREST = NEAREST;
+ public static final int _wGL_CLAMP = CLAMP_TO_EDGE;
+ public static final int _wGL_TEXTURE_WRAP_S = TEXTURE_WRAP_S;
+ public static final int _wGL_TEXTURE_WRAP_T = TEXTURE_WRAP_T;
+ public static final int _wGL_REPEAT = REPEAT;
+ public static final int _wGL_DST_COLOR = DST_COLOR;
+ public static final int _wGL_DST_ALPHA = DST_ALPHA;
+ public static final int _wGL_FLOAT = FLOAT;
+ public static final int _wGL_SHORT = SHORT;
+ public static final int _wGL_TRIANGLES = TRIANGLES;
+ public static final int _wGL_TRIANGLE_STRIP = TRIANGLE_STRIP;
+ public static final int _wGL_TRIANGLE_FAN = TRIANGLE_FAN;
+ public static final int _wGL_LINE_STRIP = LINE_STRIP;
+ public static final int _wGL_LINES = LINES;
+ public static final int _wGL_PACK_ALIGNMENT = PACK_ALIGNMENT;
+ public static final int _wGL_UNPACK_ALIGNMENT = UNPACK_ALIGNMENT;
+ public static final int _wGL_TEXTURE0 = TEXTURE0;
+ public static final int _wGL_TEXTURE1 = TEXTURE1;
+ public static final int _wGL_TEXTURE2 = TEXTURE2;
+ public static final int _wGL_TEXTURE3 = TEXTURE3;
+ public static final int _wGL_VIEWPORT = VIEWPORT;
+ public static final int _wGL_VERTEX_SHADER = VERTEX_SHADER;
+ public static final int _wGL_FRAGMENT_SHADER = FRAGMENT_SHADER;
+ public static final int _wGL_ARRAY_BUFFER = ARRAY_BUFFER;
+ public static final int _wGL_ELEMENT_ARRAY_BUFFER = ELEMENT_ARRAY_BUFFER;
+ public static final int _wGL_STATIC_DRAW = STATIC_DRAW;
+ public static final int _wGL_DYNAMIC_DRAW = DYNAMIC_DRAW;
+ public static final int _wGL_STREAM_DRAW = STREAM_DRAW;
+ public static final int _wGL_INVALID_ENUM = INVALID_ENUM;
+ public static final int _wGL_INVALID_VALUE= INVALID_VALUE;
+ public static final int _wGL_INVALID_OPERATION = INVALID_OPERATION;
+ public static final int _wGL_OUT_OF_MEMORY = OUT_OF_MEMORY;
+ public static final int _wGL_CONTEXT_LOST_WEBGL = CONTEXT_LOST_WEBGL;
+ public static final int _wGL_FRAMEBUFFER_COMPLETE = FRAMEBUFFER_COMPLETE;
+ public static final int _wGL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT = FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
+ public static final int _wGL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT;
+ public static final int _wGL_COLOR_ATTACHMENT0 = COLOR_ATTACHMENT0;
+ public static final int _wGL_DEPTH_STENCIL_ATTACHMENT = DEPTH_STENCIL_ATTACHMENT;
+ public static final int _wGL_DEPTH_STENCIL = DEPTH_STENCIL;
+ public static final int _wGL_NEAREST_MIPMAP_LINEAR = NEAREST_MIPMAP_LINEAR;
+ public static final int _wGL_LINEAR_MIPMAP_LINEAR = LINEAR_MIPMAP_LINEAR;
+ public static final int _wGL_LINEAR_MIPMAP_NEAREST = LINEAR_MIPMAP_NEAREST;
+ public static final int _wGL_NEAREST_MIPMAP_NEAREST = NEAREST_MIPMAP_NEAREST;
+ public static final int _wGL_TEXTURE_MAX_LEVEL = TEXTURE_MAX_LEVEL;
+ public static final int _wGL_UNSIGNED_INT_24_8 = UNSIGNED_INT_24_8;
+ public static final int _wGL_UNSIGNED_INT = UNSIGNED_INT;
+ public static final int _wGL_ANY_SAMPLES_PASSED = ANY_SAMPLES_PASSED;
+ public static final int _wGL_QUERY_RESULT = QUERY_RESULT;
+ public static final int _wGL_QUERY_RESULT_AVAILABLE = QUERY_RESULT_AVAILABLE;
+ public static final int _wGL_TEXTURE_MAX_ANISOTROPY = TEXTURE_MAX_ANISOTROPY_EXT;
+ public static final int _wGL_DEPTH24_STENCIL8 = DEPTH24_STENCIL8;
+ public static final int _wGL_DEPTH_COMPONENT32F = DEPTH_COMPONENT32F;
+ public static final int _wGL_DEPTH_ATTACHMENT = DEPTH_ATTACHMENT;
+ public static final int _wGL_MULTISAMPLE = -1;
+ public static final int _wGL_LINE_SMOOTH = -1;
+ public static final int _wGL_READ_FRAMEBUFFER = READ_FRAMEBUFFER;
+ public static final int _wGL_DRAW_FRAMEBUFFER = DRAW_FRAMEBUFFER;
+ public static final int _wGL_FRAMEBUFFER = FRAMEBUFFER;
+ public static final int _wGL_POLYGON_OFFSET_FILL = POLYGON_OFFSET_FILL;
+
+ public static final class TextureGL {
+ protected final WebGLTexture obj;
+ public int w = -1;
+ public int h = -1;
+ public boolean nearest = true;
+ public boolean anisotropic = false;
+ protected TextureGL(WebGLTexture obj) {
+ this.obj = obj;
+ }
+ }
+ public static final class BufferGL {
+ protected final WebGLBuffer obj;
+ protected BufferGL(WebGLBuffer obj) {
+ this.obj = obj;
+ }
+ }
+ public static final class ShaderGL {
+ protected final WebGLShader obj;
+ protected ShaderGL(WebGLShader obj) {
+ this.obj = obj;
+ }
+ }
+ private static int progId = 0;
+ public static final class ProgramGL {
+ protected final WebGLProgram obj;
+ protected final int hashcode;
+ protected ProgramGL(WebGLProgram obj) {
+ this.obj = obj;
+ this.hashcode = ++progId;
+ }
+ }
+ public static final class UniformGL {
+ protected final WebGLUniformLocation obj;
+ protected UniformGL(WebGLUniformLocation obj) {
+ this.obj = obj;
+ }
+ }
+ public static final class BufferArrayGL {
+ protected final WebGLVertexArray obj;
+ public boolean isQuadBufferBound;
+ protected BufferArrayGL(WebGLVertexArray obj) {
+ this.obj = obj;
+ this.isQuadBufferBound = false;
+ }
+ }
+ public static final class FramebufferGL {
+ protected final WebGLFramebuffer obj;
+ protected FramebufferGL(WebGLFramebuffer obj) {
+ this.obj = obj;
+ }
+ }
+ public static final class RenderbufferGL {
+ protected final WebGLRenderbuffer obj;
+ protected RenderbufferGL(WebGLRenderbuffer obj) {
+ this.obj = obj;
+ }
+ }
+ public static final class QueryGL {
+ protected final WebGLQuery obj;
+ protected QueryGL(WebGLQuery obj) {
+ this.obj = obj;
+ }
+ }
+
+ public static final boolean anisotropicFilteringSupported() {
+ return anisotropicFilteringSupported;
+ }
+ public static final void _wglEnable(int p1) {
+ webgl.enable(p1);
+ }
+ public static final void _wglClearDepth(float p1) {
+ webgl.clearDepth(p1);
+ }
+ public static final void _wglDepthFunc(int p1) {
+ webgl.depthFunc(p1);
+ }
+ public static final void _wglCullFace(int p1) {
+ webgl.cullFace(p1);
+ }
+ private static int[] viewportCache = new int[4];
+ public static final void _wglViewport(int p1, int p2, int p3, int p4) {
+ viewportCache[0] = p1; viewportCache[1] = p2;
+ viewportCache[2] = p3; viewportCache[3] = p4;
+ webgl.viewport(p1, p2, p3, p4);
+ }
+ public static final void _wglClear(int p1) {
+ webgl.clear(p1);
+ }
+ public static final void _wglClearColor(float p1, float p2, float p3, float p4) {
+ webgl.clearColor(p1, p2, p3, p4);
+ }
+ public static final void _wglDisable(int p1) {
+ webgl.disable(p1);
+ }
+ public static final int _wglGetError() {
+ return webgl.getError();
+ }
+ public static final void _wglFlush() {
+ //webgl.flush();
+ }
+ public static final void _wglTexImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ByteBuffer p9) {
+ webgl.texImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertByteBufferUnsigned(p9) : null);
+ }
+ public static final void _wglBlendFunc(int p1, int p2) {
+ webgl.blendFunc(p1, p2);
+ }
+ public static final void _wglBlendFuncSeparate(int p1, int p2, int p3, int p4) {
+ webgl.blendFuncSeparate(p1, p2, p3, p4);
+ }
+ public static final void _wglBlendColor(float r, float g, float b, float a) {
+ webgl.blendColor(r, g, b, a);
+ }
+ public static final void _wglDepthMask(boolean p1) {
+ webgl.depthMask(p1);
+ }
+ public static final void _wglColorMask(boolean p1, boolean p2, boolean p3, boolean p4) {
+ webgl.colorMask(p1, p2, p3, p4);
+ }
+ public static final void _wglBindTexture(int p1, TextureGL p2) {
+ webgl.bindTexture(p1, p2 == null ? null : p2.obj);
+ }
+ public static final void _wglCopyTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {
+ webgl.copyTexSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8);
+ }
+ public static final void _wglTexParameteri(int p1, int p2, int p3) {
+ webgl.texParameteri(p1, p2, p3);
+ }
+ public static final void _wglTexParameterf(int p1, int p2, float p3) {
+ webgl.texParameterf(p1, p2, p3);
+ }
+ public static final void _wglTexImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, IntBuffer p9) {
+ webgl.texImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertIntBufferUnsigned(p9) : null);
+ }
+ public static final void _wglTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, IntBuffer p9) {
+ webgl.texSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertIntBufferUnsigned(p9) : null);
+ }
+ public static final void _wglDeleteTextures(TextureGL p1) {
+ webgl.deleteTexture(p1.obj);
+ }
+ public static final void _wglDrawArrays(int p1, int p2, int p3) {
+ webgl.drawArrays(p1, p2, p3);
+ }
+ public static final void _wglDrawElements(int p1, int p2, int p3, int p4) {
+ webgl.drawElements(p1, p2, p3, p4);
+ }
+ public static final TextureGL _wglGenTextures() {
+ return new TextureGL(webgl.createTexture());
+ }
+ public static final void _wglTexSubImage2D(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, ByteBuffer p9) {
+ webgl.texSubImage2D(p1, p2, p3, p4, p5, p6, p7, p8, p9 != null ? BufferConverter.convertByteBufferUnsigned(p9) : null);
+ }
+ public static final void _wglActiveTexture(int p1) {
+ webgl.activeTexture(p1);
+ }
+ public static final ProgramGL _wglCreateProgram() {
+ return new ProgramGL(webgl.createProgram());
+ }
+ public static final ShaderGL _wglCreateShader(int p1) {
+ return new ShaderGL(webgl.createShader(p1));
+ }
+ public static final void _wglAttachShader(ProgramGL p1, ShaderGL p2) {
+ webgl.attachShader(p1.obj, p2.obj);
+ }
+ public static final void _wglDetachShader(ProgramGL p1, ShaderGL p2) {
+ webgl.detachShader(p1.obj, p2.obj);
+ }
+ public static final void _wglCompileShader(ShaderGL p1) {
+ webgl.compileShader(p1.obj);
+ }
+ public static final void _wglLinkProgram(ProgramGL p1) {
+ webgl.linkProgram(p1.obj);
+ }
+ public static final void _wglShaderSource(ShaderGL p1, String p2) {
+ webgl.shaderSource(p1.obj, p2);
+ }
+ public static final String _wglGetShaderInfoLog(ShaderGL p1) {
+ return webgl.getShaderInfoLog(p1.obj);
+ }
+ public static final String _wglGetProgramInfoLog(ProgramGL p1) {
+ return webgl.getProgramInfoLog(p1.obj);
+ }
+ public static final boolean _wglGetShaderCompiled(ShaderGL p1) {
+ return webgl.getShaderParameteri(p1.obj, COMPILE_STATUS) == 1;
+ }
+ public static final boolean _wglGetProgramLinked(ProgramGL p1) {
+ return webgl.getProgramParameteri(p1.obj, LINK_STATUS) == 1;
+ }
+ public static final void _wglDeleteShader(ShaderGL p1) {
+ webgl.deleteShader(p1.obj);
+ }
+ public static final void _wglDeleteProgram(ProgramGL p1) {
+ webgl.deleteProgram(p1.obj);
+ }
+ public static final BufferGL _wglCreateBuffer() {
+ return new BufferGL(webgl.createBuffer());
+ }
+ public static final void _wglDeleteBuffer(BufferGL p1) {
+ webgl.deleteBuffer(p1.obj);
+ }
+ public static final void _wglBindBuffer(int p1, BufferGL p2) {
+ webgl.bindBuffer(p1, p2 == null ? null : p2.obj);
+ }
+ public static final void _wglBufferData0(int p1, IntBuffer p2, int p3) {
+ webgl.bufferData(p1, p2 != null ? BufferConverter.convertIntBufferUnsigned(p2) : null, p3);
+ }
+ public static final void _wglBufferSubData0(int p1, int p2, IntBuffer p3) {
+ webgl.bufferSubData(p1, p2, p3 != null ? BufferConverter.convertIntBufferUnsigned(p3) : null);
+ }
+ public static final void _wglBufferData(int p1, Object p2, int p3) {
+ webgl.bufferData(p1, (Int32Array)p2, p3);
+ }
+ public static final void _wglBufferData00(int p1, long len, int p3) {
+ webgl.bufferData(p1, (int)len, p3);
+ }
+ public static final void _wglBufferSubData(int p1, int p2, Object p3) {
+ webgl.bufferSubData(p1, p2, (Int32Array)p3);
+ }
+ public static final void _wglBindAttribLocation(ProgramGL p1, int p2, String p3) {
+ webgl.bindAttribLocation(p1.obj, p2, p3);
+ }
+ public static final void _wglEnableVertexAttribArray(int p1) {
+ webgl.enableVertexAttribArray(p1);
+ }
+ public static final void _wglDisableVertexAttribArray(int p1) {
+ webgl.disableVertexAttribArray(p1);
+ }
+ public static final UniformGL _wglGetUniformLocation(ProgramGL p1, String p2) {
+ WebGLUniformLocation u = webgl.getUniformLocation(p1.obj, p2);
+ return u == null ? null : new UniformGL(u);
+ }
+ public static final void _wglBindAttributeLocation(ProgramGL p1, int p2, String p3) {
+ webgl.bindAttribLocation(p1.obj, p2, p3);
+ }
+ public static final void _wglUniform1f(UniformGL p1, float p2) {
+ if(p1 != null) webgl.uniform1f(p1.obj, p2);
+ }
+ public static final void _wglUniform2f(UniformGL p1, float p2, float p3) {
+ if(p1 != null) webgl.uniform2f(p1.obj, p2, p3);
+ }
+ public static final void _wglUniform3f(UniformGL p1, float p2, float p3, float p4) {
+ if(p1 != null) webgl.uniform3f(p1.obj, p2, p3, p4);
+ }
+ public static final void _wglUniform4f(UniformGL p1, float p2, float p3, float p4, float p5) {
+ if(p1 != null) webgl.uniform4f(p1.obj, p2, p3, p4, p5);
+ }
+ public static final void _wglUniform1i(UniformGL p1, int p2) {
+ if(p1 != null) webgl.uniform1i(p1.obj, p2);
+ }
+ public static final void _wglUniform2i(UniformGL p1, int p2, int p3) {
+ if(p1 != null) webgl.uniform2i(p1.obj, p2, p3);
+ }
+ public static final void _wglUniform3i(UniformGL p1, int p2, int p3, int p4) {
+ if(p1 != null) webgl.uniform3i(p1.obj, p2, p3, p4);
+ }
+ public static final void _wglUniform4i(UniformGL p1, int p2, int p3, int p4, int p5) {
+ if(p1 != null) webgl.uniform4i(p1.obj, p2, p3, p4, p5);
+ }
+ public static final void _wglUniformMat2fv(UniformGL p1, float[] mat) {
+ if(p1 != null) webgl.uniformMatrix2fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
+ }
+ public static final void _wglUniformMat3fv(UniformGL p1, float[] mat) {
+ if(p1 != null) webgl.uniformMatrix3fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
+ }
+ public static final void _wglUniformMat4fv(UniformGL p1, float[] mat) {
+ if(p1 != null) webgl.uniformMatrix4fv(p1.obj, false, TeaVMUtils.unwrapFloatArray(mat));
+ }
+ private static int currentProgram = -1;
+ public static final void _wglUseProgram(ProgramGL p1) {
+ if(p1 != null && currentProgram != p1.hashcode) {
+ currentProgram = p1.hashcode;
+ webgl.useProgram(p1.obj);
+ }
+ }
+ public static final void _wglGetParameter(int p1, int size, int[] ret) {
+ if(p1 == _wGL_VIEWPORT) {
+ ret[0] = viewportCache[0];
+ ret[1] = viewportCache[1];
+ ret[2] = viewportCache[2];
+ ret[3] = viewportCache[3];
+ }
+ }
+ public static final void _wglPolygonOffset(float p1, float p2) {
+ webgl.polygonOffset(p1, p2);
+ }
+ public static final void _wglVertexAttribPointer(int p1, int p2, int p3, boolean p4, int p5, int p6) {
+ webgl.vertexAttribPointer(p1, p2, p3, p4, p5, p6);
+ }
+ public static final void _wglBindFramebuffer(int p1, FramebufferGL p2) {
+ webgl.bindFramebuffer(p1, p2 == null ? backBuffer.obj : p2.obj);
+ }
+ public static final void _wglReadBuffer(int p1) {
+ webgl.readBuffer(p1);
+ }
+ public static final FramebufferGL _wglCreateFramebuffer() {
+ return new FramebufferGL(webgl.createFramebuffer());
+ }
+ public static final void _wglDeleteFramebuffer(FramebufferGL p1) {
+ webgl.deleteFramebuffer(p1.obj);
+ }
+ public static final void _wglFramebufferTexture2D(int p1, TextureGL p2) {
+ webgl.framebufferTexture2D(FRAMEBUFFER, p1, TEXTURE_2D, p2 == null ? null : p2.obj, 0);
+ }
+ public static final void _wglFramebufferTexture2D(int p1, TextureGL p2, int p3) {
+ webgl.framebufferTexture2D(FRAMEBUFFER, p1, TEXTURE_2D, p2 == null ? null : p2.obj, p3);
+ }
+ public static final QueryGL _wglCreateQuery() {
+ return new QueryGL(webgl.createQuery());
+ }
+ public static final void _wglBeginQuery(int p1, QueryGL p2) {
+ webgl.beginQuery(p1, p2.obj);
+ }
+ public static final void _wglEndQuery(int p1) {
+ webgl.endQuery(p1);
+ }
+ public static final void _wglDeleteQuery(QueryGL p1) {
+ webgl.deleteQuery(p1.obj);
+ }
+ public static final int _wglGetQueryObjecti(QueryGL p1, int p2) {
+ return webgl.getQueryParameter(p1.obj, p2);
+ }
+ public static final BufferArrayGL _wglCreateVertexArray() {
+ return new BufferArrayGL(webgl.createVertexArray());
+ }
+ public static final void _wglDeleteVertexArray(BufferArrayGL p1) {
+ webgl.deleteVertexArray(p1.obj);
+ }
+ public static final void _wglBindVertexArray(BufferArrayGL p1) {
+ webgl.bindVertexArray(p1 == null ? null : p1.obj);
+ }
+ public static final void _wglDrawBuffer(int p1) {
+ webgl.drawBuffers(new int[] { p1 });
+ }
+ public static final RenderbufferGL _wglCreateRenderBuffer() {
+ return new RenderbufferGL(webgl.createRenderbuffer());
+ }
+ public static final void _wglBindRenderbuffer(RenderbufferGL p1) {
+ webgl.bindRenderbuffer(RENDERBUFFER, p1 == null ? null : p1.obj);
+ }
+ public static final void _wglRenderbufferStorage(int p1, int p2, int p3) {
+ webgl.renderbufferStorage(RENDERBUFFER, p1, p2, p3);
+ }
+ public static final void _wglFramebufferRenderbuffer(int p1, RenderbufferGL p2) {
+ webgl.framebufferRenderbuffer(FRAMEBUFFER, p1, RENDERBUFFER, p2 == null ? null : p2.obj);
+ }
+ public static final void _wglDeleteRenderbuffer(RenderbufferGL p1) {
+ webgl.deleteRenderbuffer(p1.obj);
+ }
+ public static final void _wglRenderbufferStorageMultisample(int p1, int p2, int p3, int p4) {
+ webgl.renderbufferStorageMultisample(RENDERBUFFER, p1, p2, p3, p4);
+ }
+ public static final void _wglBlitFramebuffer(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, int p9, int p10) {
+ webgl.blitFramebuffer(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
+ }
+ public static final int _wglGetAttribLocation(ProgramGL p1, String p2) {
+ return webgl.getAttribLocation(p1.obj, p2);
+ }
+
+ @JSBody(params = { "ctx", "p" }, script = "return ctx.getTexParameter(0x0DE1, p) | 0;")
+ private static final native int __wglGetTexParameteri(WebGL2RenderingContext ctx, int p);
+ public static final int _wglGetTexParameteri(int p1) {
+ return __wglGetTexParameteri(webgl, p1);
+ }
+ @JSBody(params = { "ctx", "p" }, script = "return (0.0 + ctx.getTexParameter(0x0DE1, p));")
+ private static final native float __wglGetTexParameterf(WebGL2RenderingContext ctx, int p);
+ public static final float _wglGetTexParameterf(int p1) {
+ return __wglGetTexParameterf(webgl, p1);
+ }
+ public static final boolean isWindows() {
+ return getNavString("platform").toLowerCase().contains("win");
+ }
+ public static final boolean glNeedsAnisotropicFix() {
+ return anisotropicFilteringSupported && DetectAnisotropicGlitch.hasGlitch();
+ }
+
+ private static HTMLCanvasElement imageLoadCanvas = null;
+ private static CanvasRenderingContext2D imageLoadContext = null;
+
+ @JSBody(params = { "buf", "mime" }, script = "return URL.createObjectURL(new Blob([buf], {type: mime}));")
+ private static native String getDataURL(ArrayBuffer buf, String mime);
+
+ @JSBody(params = { "url" }, script = "URL.revokeObjectURL(url);")
+ private static native void freeDataURL(String url);
+
+ public static final EaglerImage loadPNG(byte[] data) {
+ return loadPNG0(TeaVMUtils.unwrapArrayBuffer(data));
+ }
+
+ @JSBody(params = { "cccc", "ennn" }, script = "cccc.imageSmoothingEnabled = ennn;")
+ private static native void setImageSmoothingMode(CanvasRenderingContext2D cc, boolean en);
+
+ @Async
+ private static native EaglerImage loadPNG0(ArrayBuffer data);
+
+ private static void loadPNG0(ArrayBuffer data, final AsyncCallback ret) {
+ final HTMLImageElement toLoad = (HTMLImageElement) doc.createElement("img");
+ toLoad.addEventListener("load", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ if(imageLoadCanvas == null) {
+ imageLoadCanvas = (HTMLCanvasElement) doc.createElement("canvas");
+ }
+ if(imageLoadCanvas.getWidth() < toLoad.getWidth()) {
+ imageLoadCanvas.setWidth(toLoad.getWidth());
+ }
+ if(imageLoadCanvas.getHeight() < toLoad.getHeight()) {
+ imageLoadCanvas.setHeight(toLoad.getHeight());
+ }
+ if(imageLoadContext == null) {
+ imageLoadContext = (CanvasRenderingContext2D) imageLoadCanvas.getContext("2d", youEagler2());
+ setImageSmoothingMode(imageLoadContext, false);
+ }
+ imageLoadContext.clearRect(0, 0, toLoad.getWidth(), toLoad.getHeight());
+ imageLoadContext.drawImage(toLoad, 0, 0, toLoad.getWidth(), toLoad.getHeight());
+ ImageData pxlsDat = imageLoadContext.getImageData(0, 0, toLoad.getWidth(), toLoad.getHeight());
+ Uint8ClampedArray pxls = pxlsDat.getData();
+ int totalPixels = pxlsDat.getWidth() * pxlsDat.getHeight();
+ freeDataURL(toLoad.getSrc());
+ if(pxls.getByteLength() < totalPixels << 2) {
+ ret.complete(null);
+ return;
+ }
+ DataView dv = new DataView(pxls.getBuffer());
+ int[] pixels = new int[totalPixels];
+ for(int i = 0, j; i < pixels.length; ++i) {
+ j = dv.getUint32(i << 2, false);
+ pixels[i] = ((j >> 8) & 0xFFFFFF) | ((j & 0xFF) << 24);
+ }
+ ret.complete(new EaglerImage(pixels, pxlsDat.getWidth(), pxlsDat.getHeight(), true));
+ }
+ });
+ toLoad.addEventListener("error", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ freeDataURL(toLoad.getSrc());
+ ret.complete(null);
+ }
+ });
+ String src = getDataURL(data, "image/png");
+ if(src == null) {
+ ret.complete(null);
+ }else {
+ toLoad.setSrc(src);
+ }
+ }
+
+ private static HTMLVideoElement currentVideo = null;
+ private static TextureGL videoTexture = null;
+ private static boolean videoIsLoaded = false;
+ private static boolean videoTexIsInitialized = false;
+ private static int frameRate = 33;
+ private static long frameTimer = 0l;
+
+ public static final boolean isVideoSupported() {
+ return true;
+ }
+ public static final void loadVideo(String src, boolean autoplay) {
+ loadVideo(src, autoplay, null, null);
+ }
+ public static final void loadVideo(String src, boolean autoplay, String setJavascriptPointer) {
+ loadVideo(src, autoplay, setJavascriptPointer, null);
+ }
+
+ @JSBody(params = { "ptr", "el" }, script = "window[ptr] = el;")
+ private static native void setVideoPointer(String ptr, HTMLVideoElement el);
+ @JSBody(params = { "ptr", "el" }, script = "window[ptr](el);")
+ private static native void callVideoLoadEvent(String ptr, HTMLVideoElement el);
+
+ private static MediaElementAudioSourceNode currentVideoAudioSource = null;
+
+ private static GainNode currentVideoAudioGain = null;
+ private static float currentVideoAudioGainValue = 1.0f;
+
+ private static PannerNode currentVideoAudioPanner = null;
+ private static float currentVideoAudioX = 0.0f;
+ private static float currentVideoAudioY = 0.0f;
+ private static float currentVideoAudioZ = 0.0f;
+
+ public static final void loadVideo(String src, boolean autoplay, String setJavascriptPointer, final String javascriptOnloadFunction) {
+ videoIsLoaded = false;
+ videoTexIsInitialized = false;
+ if(videoTexture == null) {
+ videoTexture = _wglGenTextures();
+ }
+ if(currentVideo != null) {
+ currentVideo.pause();
+ currentVideo.setSrc("");
+ }
+
+ BufferedVideo vid = videosBuffer.get(src);
+
+ if(vid != null) {
+ currentVideo = vid.videoElement;
+ videosBuffer.remove(src);
+ }else {
+ currentVideo = (HTMLVideoElement) win.getDocument().createElement("video");
+ currentVideo.setAttribute("crossorigin", "anonymous");
+ currentVideo.setAutoplay(autoplay);
+ }
+
+ if(setJavascriptPointer != null) {
+ setVideoPointer(setJavascriptPointer, currentVideo);
+ }
+
+ currentVideo.addEventListener("playing", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ videoIsLoaded = true;
+ if(javascriptOnloadFunction != null) {
+ callVideoLoadEvent(javascriptOnloadFunction, currentVideo);
+ }
+ }
+ });
+
+ if(vid == null) {
+ currentVideo.setControls(false);
+ currentVideo.setSrc(src);
+ }else {
+ if(autoplay) {
+ currentVideo.play();
+ }
+ }
+
+ if(currentVideoAudioSource != null) {
+ currentVideoAudioSource.disconnect();
+ }
+
+ currentVideoAudioSource = audioctx.createMediaElementSource(currentVideo);
+
+ if(currentVideoAudioGainValue < 0.0f) {
+ currentVideoAudioSource.connect(masterVolumeNode);
+ }else {
+ if(currentVideoAudioGain == null) {
+ currentVideoAudioGain = audioctx.createGain();
+ currentVideoAudioGain.getGain().setValue(currentVideoAudioGainValue > 1.0f ? 1.0f : currentVideoAudioGainValue);
+ }
+
+ currentVideoAudioSource.connect(currentVideoAudioGain);
+
+ if(currentVideoAudioPanner == null) {
+ currentVideoAudioPanner = audioctx.createPanner();
+ currentVideoAudioPanner.setRolloffFactor(1f);
+ currentVideoAudioPanner.setDistanceModel("linear");
+ currentVideoAudioPanner.setPanningModel("HRTF");
+ currentVideoAudioPanner.setConeInnerAngle(360f);
+ currentVideoAudioPanner.setConeOuterAngle(0f);
+ currentVideoAudioPanner.setConeOuterGain(0f);
+ currentVideoAudioPanner.setOrientation(0f, 1f, 0f);
+ currentVideoAudioPanner.setPosition(currentVideoAudioX, currentVideoAudioY, currentVideoAudioZ);
+ currentVideoAudioPanner.setMaxDistance(currentVideoAudioGainValue * 16f + 0.1f);
+ currentVideoAudioGain.connect(currentVideoAudioPanner);
+ currentVideoAudioPanner.connect(audioctx.getDestination());
+ }
+ }
+
+ }
+
+ private static class BufferedVideo {
+
+ protected final HTMLVideoElement videoElement;
+ protected final String url;
+ protected final long requestedTime;
+ protected final int ttl;
+
+ public BufferedVideo(HTMLVideoElement videoElement, String url, int ttl) {
+ this.videoElement = videoElement;
+ this.url = url;
+ this.requestedTime = steadyTimeMillis();
+ this.ttl = ttl;
+ }
+
+ }
+
+ private static final HashMap videosBuffer = new HashMap<>();
+
+ public static final void bufferVideo(String src, int ttl) {
+ if(!videosBuffer.containsKey(src)) {
+ HTMLVideoElement video = (HTMLVideoElement) win.getDocument().createElement("video");
+ video.setAutoplay(false);
+ video.setAttribute("crossorigin", "anonymous");
+ video.setPreload("auto");
+ video.setControls(false);
+ video.setSrc(src);
+ videosBuffer.put(src, new BufferedVideo(video, src, ttl));
+ }
+ }
+
+ public static final void unloadVideo() {
+ if(videoTexture != null) {
+ _wglDeleteTextures(videoTexture);
+ videoTexture = null;
+ }
+ if(currentVideo != null) {
+ currentVideo.pause();
+ currentVideo.setSrc("");
+ currentVideo = null;
+ }
+ if(currentVideoAudioSource != null) {
+ currentVideoAudioSource.disconnect();
+ }
+ }
+ public static final boolean isVideoLoaded() {
+ return videoTexture != null && currentVideo != null && videoIsLoaded;
+ }
+ public static final boolean isVideoPaused() {
+ return currentVideo == null || currentVideo.isPaused();
+ }
+ public static final void setVideoPaused(boolean pause) {
+ if(currentVideo != null) {
+ if(pause) {
+ currentVideo.pause();
+ }else {
+ currentVideo.play();
+ }
+ }
+ }
+ public static final void setVideoLoop(boolean loop) {
+ if(currentVideo != null) {
+ currentVideo.setLoop(loop);
+ }
+ }
+ public static final void setVideoVolume(float x, float y, float z, float v) {
+ currentVideoAudioX = x;
+ currentVideoAudioY = y;
+ currentVideoAudioZ = z;
+ if(v < 0.0f) {
+ if(currentVideoAudioGainValue >= 0.0f && currentVideoAudioSource != null) {
+ currentVideoAudioSource.disconnect();
+ currentVideoAudioSource.connect(masterVolumeNode);
+ }
+ currentVideoAudioGainValue = v;
+ }else {
+ if(currentVideoAudioGain != null) {
+ currentVideoAudioGain.getGain().setValue(v > 1.0f ? 1.0f : v);
+ if(currentVideoAudioGainValue < 0.0f && currentVideoAudioSource != null) {
+ currentVideoAudioSource.disconnect();
+ currentVideoAudioSource.connect(currentVideoAudioGain);
+ }
+ }
+ currentVideoAudioGainValue = v;
+ if(currentVideoAudioPanner != null) {
+ currentVideoAudioPanner.setMaxDistance(v * 16f + 0.1f);
+ currentVideoAudioPanner.setPosition(x, y, z);
+ }
+ }
+ }
+
+ @JSBody(
+ params = {"ctx", "target", "internalformat", "format", "type", "video"},
+ script = "ctx.texImage2D(target, 0, internalformat, format, type, video);"
+ )
+ private static native void html5VideoTexImage2D(WebGL2RenderingContext ctx, int target, int internalformat, int format, int type, HTMLVideoElement video);
+
+ @JSBody(
+ params = {"ctx", "target", "format", "type", "video"},
+ script = "ctx.texSubImage2D(target, 0, 0, 0, format, type, video);"
+ )
+ private static native void html5VideoTexSubImage2D(WebGL2RenderingContext ctx, int target, int format, int type, HTMLVideoElement video);
+
+ public static final void updateVideoTexture() {
+ long ms = steadyTimeMillis();
+ if(ms - frameTimer < frameRate && videoTexIsInitialized) {
+ return;
+ }
+ frameTimer = ms;
+ if(currentVideo != null && videoTexture != null && videoIsLoaded) {
+ try {
+ _wglBindTexture(_wGL_TEXTURE_2D, videoTexture);
+ if(videoTexIsInitialized) {
+ html5VideoTexSubImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentVideo);
+ }else {
+ html5VideoTexImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentVideo);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_S, _wGL_CLAMP);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_T, _wGL_CLAMP);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MIN_FILTER, _wGL_LINEAR);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MAG_FILTER, _wGL_LINEAR);
+ videoTexIsInitialized = true;
+ }
+ }catch(Throwable t) {
+ // rip
+ }
+ }
+ }
+ public static final void bindVideoTexture() {
+ if(videoTexture != null) {
+ _wglBindTexture(_wGL_TEXTURE_2D, videoTexture);
+ }
+ }
+ public static final int getVideoWidth() {
+ if(currentVideo != null && videoIsLoaded) {
+ return currentVideo.getWidth();
+ }else {
+ return -1;
+ }
+ }
+ public static final int getVideoHeight() {
+ if(currentVideo != null && videoIsLoaded) {
+ return currentVideo.getHeight();
+ }else {
+ return -1;
+ }
+ }
+ public static final float getVideoCurrentTime() {
+ if(currentVideo != null && videoIsLoaded) {
+ return (float) currentVideo.getCurrentTime();
+ }else {
+ return -1.0f;
+ }
+ }
+ public static final void setVideoCurrentTime(float seconds) {
+ if(currentVideo != null && videoIsLoaded) {
+ currentVideo.setCurrentTime(seconds);
+ }
+ }
+ public static final float getVideoDuration() {
+ if(currentVideo != null && videoIsLoaded) {
+ return (float) currentVideo.getDuration();
+ }else {
+ return -1.0f;
+ }
+ }
+
+ public static final int VIDEO_ERR_NONE = -1;
+ public static final int VIDEO_ERR_ABORTED = 1;
+ public static final int VIDEO_ERR_NETWORK = 2;
+ public static final int VIDEO_ERR_DECODE = 3;
+ public static final int VIDEO_ERR_SRC_NOT_SUPPORTED = 4;
+
+ public static final int getVideoError() {
+ if(currentVideo != null && videoIsLoaded) {
+ MediaError err = currentVideo.getError();
+ if(err != null) {
+ return err.getCode();
+ }else {
+ return -1;
+ }
+ }else {
+ return -1;
+ }
+ }
+
+ public static final void setVideoFrameRate(float fps) {
+ frameRate = (int)(1000.0f / fps);
+ if(frameRate < 1) {
+ frameRate = 1;
+ }
+ }
+
+ private static HTMLImageElement currentImage = null;
+ private static TextureGL imageTexture = null;
+ private static boolean imageIsLoaded = false;
+ private static boolean imageTexIsInitialized = false;
+ private static int imageFrameRate = 33;
+ private static long imageFrameTimer = 0l;
+
+ public static final boolean isImageSupported() {
+ return true;
+ }
+ public static final void loadImage(String src) {
+ loadImage(src, null);
+ }
+ public static final void loadImage(String src, String setJavascriptPointer) {
+ loadImage(src, setJavascriptPointer, null);
+ }
+
+ @JSBody(params = { "ptr", "el" }, script = "window[ptr] = el;")
+ private static native void setImagePointer(String ptr, HTMLImageElement el);
+ @JSBody(params = { "ptr", "el" }, script = "window[ptr](el);")
+ private static native void callImageLoadEvent(String ptr, HTMLImageElement el);
+
+ public static final void loadImage(String src, String setJavascriptPointer, final String javascriptOnloadFunction) {
+ imageIsLoaded = false;
+ imageTexIsInitialized = false;
+ if(imageTexture == null) {
+ imageTexture = _wglGenTextures();
+ }
+ if(currentImage != null) {
+ currentImage.setSrc("");
+ }
+
+ BufferedImageElem img = imagesBuffer.get(src);
+
+ if(img != null) {
+ currentImage = img.imageElement;
+ imagesBuffer.remove(src);
+ }else {
+ currentImage = (HTMLImageElement) win.getDocument().createElement("img");
+ currentImage.setAttribute("crossorigin", "anonymous");
+ }
+
+ if(setJavascriptPointer != null) {
+ setImagePointer(setJavascriptPointer, currentImage);
+ }
+
+ currentImage.addEventListener("load", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ imageIsLoaded = true;
+ if(javascriptOnloadFunction != null) {
+ callImageLoadEvent(javascriptOnloadFunction, currentImage);
+ }
+ }
+ });
+
+ if(img == null) {
+ currentImage.setSrc(src);
+ }
+ }
+
+ private static class BufferedImageElem {
+
+ protected final HTMLImageElement imageElement;
+ protected final String url;
+ protected final long requestedTime;
+ protected final int ttl;
+
+ public BufferedImageElem(HTMLImageElement imageElement, String url, int ttl) {
+ this.imageElement = imageElement;
+ this.url = url;
+ this.requestedTime = steadyTimeMillis();
+ this.ttl = ttl;
+ }
+
+ }
+
+ private static final HashMap imagesBuffer = new HashMap<>();
+
+ public static final void bufferImage(String src, int ttl) {
+ if(!imagesBuffer.containsKey(src)) {
+ HTMLImageElement image = (HTMLImageElement) win.getDocument().createElement("img");
+ image.setAttribute("crossorigin", "anonymous");
+ image.setSrc(src);
+ imagesBuffer.put(src, new BufferedImageElem(image, src, ttl));
+ }
+ }
+
+ public static final void unloadImage() {
+ if(imageTexture != null) {
+ _wglDeleteTextures(imageTexture);
+ imageTexture = null;
+ }
+ if(currentImage != null) {
+ currentImage.setSrc("");
+ currentImage = null;
+ }
+ }
+ public static final boolean isImageLoaded() {
+ return imageTexture != null && currentImage != null && imageIsLoaded;
+ }
+
+ @JSBody(
+ params = {"ctx", "target", "internalformat", "format", "type", "image"},
+ script = "ctx.texImage2D(target, 0, internalformat, format, type, image);"
+ )
+ private static native void html5ImageTexImage2D(WebGL2RenderingContext ctx, int target, int internalformat, int format, int type, HTMLImageElement image);
+
+ @JSBody(
+ params = {"ctx", "target", "format", "type", "image"},
+ script = "ctx.texSubImage2D(target, 0, 0, 0, format, type, image);"
+ )
+ private static native void html5ImageTexSubImage2D(WebGL2RenderingContext ctx, int target, int format, int type, HTMLImageElement image);
+
+ public static final void updateImageTexture() {
+ long ms = steadyTimeMillis();
+ if(ms - imageFrameTimer < imageFrameRate && imageTexIsInitialized) {
+ return;
+ }
+ imageFrameTimer = ms;
+ if(currentImage != null && imageTexture != null && imageIsLoaded) {
+ try {
+ _wglBindTexture(_wGL_TEXTURE_2D, imageTexture);
+ if(imageTexIsInitialized) {
+ html5ImageTexSubImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentImage);
+ }else {
+ html5ImageTexImage2D(webgl, _wGL_TEXTURE_2D, _wGL_RGBA, _wGL_RGBA, _wGL_UNSIGNED_BYTE, currentImage);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_S, _wGL_CLAMP);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_WRAP_T, _wGL_CLAMP);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MIN_FILTER, _wGL_LINEAR);
+ _wglTexParameteri(_wGL_TEXTURE_2D, _wGL_TEXTURE_MAG_FILTER, _wGL_LINEAR);
+ imageTexIsInitialized = true;
+ }
+ }catch(Throwable t) {
+ // rip
+ }
+ }
+ }
+ public static final void bindImageTexture() {
+ if(imageTexture != null) {
+ _wglBindTexture(_wGL_TEXTURE_2D, imageTexture);
+ }
+ }
+ public static final int getImageWidth() {
+ if(currentImage != null && imageIsLoaded) {
+ return currentImage.getWidth();
+ }else {
+ return -1;
+ }
+ }
+ public static final int getImageHeight() {
+ if(currentImage != null && imageIsLoaded) {
+ return currentImage.getHeight();
+ }else {
+ return -1;
+ }
+ }
+
+ public static final void setImageFrameRate(float fps) {
+ frameRate = (int)(1000.0f / fps);
+ if(frameRate < 1) {
+ frameRate = 1;
+ }
+ }
+
+ private static MouseEvent currentEvent = null;
+ private static KeyboardEvent currentEventK = null;
+ private static boolean[] buttonStates = new boolean[8];
+ private static boolean[] keyStates = new boolean[256];
+ public static final boolean mouseNext() {
+ currentEvent = null;
+ return !mouseEvents.isEmpty() && (currentEvent = mouseEvents.remove(0)) != null;
+ }
+ public static final int mouseGetEventButton() {
+ if(currentEvent == null || currentEvent.getType().equals(MouseEvent.MOUSEMOVE)) return -1;
+ int b = currentEvent.getButton();
+ return b == 1 ? 2 : (b == 2 ? 1 : b);
+ }
+ public static final boolean mouseGetEventButtonState() {
+ return currentEvent == null ? false : currentEvent.getType().equals(MouseEvent.MOUSEDOWN);
+ }
+ public static final boolean mouseIsButtonDown(int p1) {
+ return buttonStates[p1];
+ }
+ public static final int mouseGetEventDWheel() {
+ return ("wheel".equals(currentEvent.getType())) ? (((WheelEvent)currentEvent).getDeltaY() == 0.0D ? 0 : (((WheelEvent)currentEvent).getDeltaY() > 0.0D ? -1 : 1)) : 0;
+ }
+ public static final void mouseSetCursorPosition(int x, int y) {
+
+ }
+ private static long mouseUngrabTimer = 0l;
+ private static int mouseUngrabTimeout = 0;
+ public static final void mouseSetGrabbed(boolean grabbed) {
+ if(grabbed) {
+ canvas.requestPointerLock();
+ long t = steadyTimeMillis();
+ if(mouseUngrabTimeout != 0) Window.clearTimeout(mouseUngrabTimeout);
+ mouseUngrabTimeout = 0;
+ if(t - mouseUngrabTimer < 3000l) {
+ mouseUngrabTimeout = Window.setTimeout(new TimerHandler() {
+ @Override
+ public void onTimer() {
+ canvas.requestPointerLock();
+ }
+ }, 3000 - (int)(t - mouseUngrabTimer));
+ }
+ }else {
+ mouseUngrabTimer = steadyTimeMillis();
+ if(mouseUngrabTimeout != 0) Window.clearTimeout(mouseUngrabTimeout);
+ mouseUngrabTimeout = 0;
+ doc.exitPointerLock();
+ }
+ }
+ public static final int mouseGetDX() {
+ double dx = mouseDX;
+ mouseDX = 0.0D;
+ return (int)dx;
+ }
+ public static final int mouseGetDY() {
+ double dy = mouseDY;
+ mouseDY = 0.0D;
+ return (int)dy;
+ }
+ public static final int mouseGetX() {
+ return mouseX;
+ }
+ public static final int mouseGetY() {
+ return mouseY;
+ }
+ public static final int mouseGetEventX() {
+ return currentEvent == null ? -1 : (int)(currentEvent.getClientX() * win.getDevicePixelRatio());
+ }
+ public static final int mouseGetEventY() {
+ return currentEvent == null ? -1 : (int)((canvas.getClientHeight() - currentEvent.getClientY()) * win.getDevicePixelRatio());
+ }
+ public static final boolean keysNext() {
+ if(unpressCTRL) { //un-press ctrl after copy/paste permission
+ keyEvents.clear();
+ currentEventK = null;
+ keyStates[29] = false;
+ keyStates[157] = false;
+ keyStates[28] = false;
+ keyStates[219] = false;
+ keyStates[220] = false;
+ unpressCTRL = false;
+ return false;
+ }
+ currentEventK = null;
+ return !keyEvents.isEmpty() && (currentEventK = keyEvents.remove(0)) != null;
+ }
+ public static final int getEventKey() {
+ return currentEventK == null ? -1 : remapKey(getWhich(currentEventK));
+ }
+ public static final char getEventChar() {
+ if(currentEventK == null) return '\0';
+ String s = currentEventK.getKey();
+ return currentEventK == null ? ' ' : (char) (s.length() > 1 ? '\0' : s.charAt(0));
+ }
+ public static final boolean getEventKeyState() {
+ return currentEventK == null? false : !currentEventK.getType().equals("keyup");
+ }
+ public static final boolean isKeyDown(int p1) {
+ if(unpressCTRL) { //un-press ctrl after copy/paste permission
+ keyStates[28] = false;
+ keyStates[29] = false;
+ keyStates[157] = false;
+ keyStates[219] = false;
+ keyStates[220] = false;
+ }
+ return keyStates[p1];
+ }
+ public static final String getKeyName(int p1) {
+ return (p1 >= 0 && p1 < 256) ? LWJGLKeyNames[p1] : "null";
+ }
+ public static final void setFullscreen(boolean p1) {
+ Window.alert("use F11 to enter fullscreen");
+ }
+ public static final boolean shouldShutdown() {
+ return false;
+ }
+ public static final boolean isVSyncSupported() {
+ return vsyncSupport;
+ }
+ @JSBody(params = { "doc" }, script = "return (typeof doc.visibilityState !== \"string\") || (doc.visibilityState === \"visible\");")
+ private static native boolean getVisibilityState(JSObject doc);
+ private static final long[] syncTimer = new long[1];
+ public static final void updateDisplay(int fpsLimit, boolean vsync) {
+ double r = win.getDevicePixelRatio();
+ int w = parent.getClientWidth();
+ int h = parent.getClientHeight();
+ int w2 = (int)(w * r);
+ int h2 = (int)(h * r);
+ if(canvas.getWidth() != w2) {
+ canvas.setWidth(w2);
+ }
+ if(canvas.getHeight() != h2) {
+ canvas.setHeight(h2);
+ }
+ webgl.bindFramebuffer(FRAMEBUFFER, null);
+ webgl.bindFramebuffer(READ_FRAMEBUFFER, backBuffer.obj);
+ webgl.bindFramebuffer(DRAW_FRAMEBUFFER, null);
+ webgl.blitFramebuffer(0, 0, backBufferWidth, backBufferHeight, 0, 0, w2, h2, COLOR_BUFFER_BIT, NEAREST);
+ webgl.bindFramebuffer(FRAMEBUFFER, backBuffer.obj);
+ resizeBackBuffer(w2, h2);
+
+ if(getVisibilityState(win.getDocument())) {
+ if(vsyncSupport && vsync) {
+ syncTimer[0] = 0l;
+ asyncRequestAnimationFrame();
+ }else {
+ if(fpsLimit <= 0) {
+ syncTimer[0] = 0l;
+ swapDelayTeaVM();
+ }else {
+ if(!EaglerAdapterGL30.sync(fpsLimit, syncTimer)) {
+ swapDelayTeaVM();
+ }
+ }
+ }
+ }else {
+ syncTimer[0] = 0l;
+ sleep(50);
+ }
+ }
+ @Async
+ private static native void asyncRequestAnimationFrame();
+ private static void asyncRequestAnimationFrame(AsyncCallback cb) {
+ if(vsyncTimeout != -1) {
+ cb.error(new IllegalStateException("Already waiting for vsync!"));
+ return;
+ }
+ final boolean[] hasTimedOut = new boolean[] { false };
+ final int[] timeout = new int[] { -1 };
+ Window.requestAnimationFrame((d) -> {
+ if(!hasTimedOut[0]) {
+ hasTimedOut[0] = true;
+ if(vsyncTimeout != -1) {
+ if(vsyncTimeout == timeout[0]) {
+ try {
+ Window.clearTimeout(vsyncTimeout);
+ }catch(Throwable t) {
+ }
+ vsyncTimeout = -1;
+ }
+ cb.complete(null);
+ }
+ }
+ });
+ vsyncTimeout = timeout[0] = Window.setTimeout(() -> {
+ if(!hasTimedOut[0]) {
+ hasTimedOut[0] = true;
+ if(vsyncTimeout != -1) {
+ vsyncTimeout = -1;
+ cb.complete(null);
+ }
+ }
+ }, 50);
+ }
+ private static final void swapDelayTeaVM() {
+ if(!useDelayOnSwap && immediateContinueChannel != null) {
+ immediateContinueTeaVM0();
+ }else {
+ sleep(0);
+ }
+ }
+ public static final void immediateContinue() {
+ if(immediateContinueChannel != null) {
+ immediateContinueTeaVM0();
+ }else {
+ sleep(0);
+ }
+ }
+ public static final boolean immediateContinueSupported() {
+ return immediateContinueChannel != null;
+ }
+ private static final JSString emptyJSString = JSString.valueOf("");
+ @Async
+ private static native void immediateContinueTeaVM0();
+ private static void immediateContinueTeaVM0(final AsyncCallback cb) {
+ if(currentMsgChannelContinueHack != null) {
+ cb.error(new IllegalStateException("Main thread is already waiting for an immediate continue callback!"));
+ return;
+ }
+ currentMsgChannelContinueHack = () -> {
+ cb.complete(null);
+ };
+ try {
+ immediateContinueChannel.getPort2().postMessage(emptyJSString);
+ }catch(Throwable t) {
+ currentMsgChannelContinueHack = null;
+ System.err.println("Caught error posting immediate continue, using setTimeout instead");
+ Window.setTimeout(() -> cb.complete(null), 0);
+ }
+ }
+ private static final int IMMEDIATE_CONT_SUPPORTED = 0;
+ private static final int IMMEDIATE_CONT_FAILED_NOT_ASYNC = 1;
+ private static final int IMMEDIATE_CONT_FAILED_NOT_CONT = 2;
+ private static final int IMMEDIATE_CONT_FAILED_EXCEPTIONS = 3;
+ private static void checkImmediateContinueSupport() {
+ immediateContinueChannel = null;
+ int stat = checkImmediateContinueSupport0();
+ if(stat == IMMEDIATE_CONT_SUPPORTED) {
+ return;
+ }else if(stat == IMMEDIATE_CONT_FAILED_NOT_ASYNC) {
+ System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to actually continuing immediately!");
+ }else if(stat == IMMEDIATE_CONT_FAILED_NOT_CONT) {
+ System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to startup check failing!");
+ }else if(stat == IMMEDIATE_CONT_FAILED_EXCEPTIONS) {
+ System.err.println("MessageChannel fast immediate continue hack is incompatible with this browser due to exceptions!");
+ }
+ immediateContinueChannel = null;
+ }
+ private static int checkImmediateContinueSupport0() {
+ try {
+ if(!MessageChannel.supported()) {
+ return IMMEDIATE_CONT_SUPPORTED;
+ }
+ immediateContinueChannel = new MessageChannel();
+ immediateContinueChannel.getPort1().addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ Runnable toRun = currentMsgChannelContinueHack;
+ currentMsgChannelContinueHack = null;
+ if(toRun != null) {
+ toRun.run();
+ }
+ }
+ });
+ immediateContinueChannel.getPort1().start();
+ immediateContinueChannel.getPort2().start();
+ final boolean[] checkMe = new boolean[1];
+ checkMe[0] = false;
+ currentMsgChannelContinueHack = () -> {
+ checkMe[0] = true;
+ };
+ immediateContinueChannel.getPort2().postMessage(emptyJSString);
+ if(checkMe[0]) {
+ currentMsgChannelContinueHack = null;
+ if(immediateContinueChannel != null) {
+ safeShutdownChannel(immediateContinueChannel);
+ }
+ immediateContinueChannel = null;
+ return IMMEDIATE_CONT_FAILED_NOT_ASYNC;
+ }
+ sleep(10);
+ currentMsgChannelContinueHack = null;
+ if(!checkMe[0]) {
+ if(immediateContinueChannel != null) {
+ safeShutdownChannel(immediateContinueChannel);
+ }
+ immediateContinueChannel = null;
+ return IMMEDIATE_CONT_FAILED_NOT_CONT;
+ }else {
+ return IMMEDIATE_CONT_SUPPORTED;
+ }
+ }catch(Throwable t) {
+ currentMsgChannelContinueHack = null;
+ if(immediateContinueChannel != null) {
+ safeShutdownChannel(immediateContinueChannel);
+ }
+ immediateContinueChannel = null;
+ return IMMEDIATE_CONT_FAILED_EXCEPTIONS;
+ }
+ }
+ private static void safeShutdownChannel(MessageChannel chan) {
+ try {
+ chan.getPort1().close();
+ }catch(Throwable tt) {
+ }
+ try {
+ chan.getPort2().close();
+ }catch(Throwable tt) {
+ }
+ }
+ public static final void setupBackBuffer() {
+ backBuffer = _wglCreateFramebuffer();
+ _wglBindFramebuffer(_wGL_FRAMEBUFFER, null);
+ backBufferColor = _wglCreateRenderBuffer();
+ _wglBindRenderbuffer(backBufferColor);
+ _wglFramebufferRenderbuffer(_wGL_COLOR_ATTACHMENT0, backBufferColor);
+ backBufferDepth = _wglCreateRenderBuffer();
+ _wglBindRenderbuffer(backBufferDepth);
+ _wglFramebufferRenderbuffer(_wGL_DEPTH_ATTACHMENT, backBufferDepth);
+ }
+ private static int backBufferWidth = -1;
+ private static int backBufferHeight = -1;
+ public static final void resizeBackBuffer(int w, int h) {
+ if(w != backBufferWidth || h != backBufferHeight) {
+ _wglBindRenderbuffer(backBufferColor);
+ _wglRenderbufferStorage(_wGL_RGBA8, w, h);
+ _wglBindRenderbuffer(backBufferDepth);
+ _wglRenderbufferStorage(_wGL_DEPTH_COMPONENT32F, w, h);
+ backBufferWidth = w;
+ backBufferHeight = h;
+ }
+ }
+ public static final float getContentScaling() {
+ return (float)win.getDevicePixelRatio();
+ }
+ public static final void enableRepeatEvents(boolean b) {
+ enableRepeatEvents = b;
+ }
+
+ @JSBody(params = { }, script = "return document.pointerLockElement != null;")
+ public static native boolean isPointerLocked();
+
+ private static boolean pointerLockFlag = false;
+
+ public static final boolean isFocused() {
+ boolean yee = isPointerLocked();
+ boolean dee = pointerLockFlag;
+ pointerLockFlag = yee;
+ if(!dee && yee) {
+ mouseDX = 0.0D;
+ mouseDY = 0.0D;
+ }
+ return isWindowFocused && !(dee && !yee);
+ }
+ public static final int getScreenWidth() {
+ return win.getScreen().getAvailWidth();
+ }
+ public static final int getScreenHeight() {
+ return win.getScreen().getAvailHeight();
+ }
+ public static final int getCanvasWidth() {
+ return canvas.getWidth();
+ }
+ public static final int getCanvasHeight() {
+ return canvas.getHeight();
+ }
+ public static final void setDisplaySize(int x, int y) {
+
+ }
+
+ private static final DateFormat dateFormatSS = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
+ public static final void saveScreenshot() {
+ webgl.finish();
+ HTMLCanvasElement retardedCanvas = (HTMLCanvasElement)doc.createElement("canvas");
+ retardedCanvas.setWidth(canvas.getWidth());
+ retardedCanvas.setHeight(canvas.getHeight());
+ CanvasRenderingContext2D cc = (CanvasRenderingContext2D)retardedCanvas.getContext("2d", youEagler2());
+ setImageSmoothingMode(cc, false);
+ cc.setFillStyle("black");
+ cc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
+ cc.drawImage(canvas, 0, 0, canvas.getWidth(), canvas.getHeight());
+ saveScreenshot("screenshot_" + dateFormatSS.format(new Date()).toString() + ".png", retardedCanvas);
+ }
+
+ @JSBody(params = { "name", "cvs" }, script = "var a=document.createElement(\"a\");a.href=cvs.toDataURL(\"image/png\");a.download=name;a.click();")
+ private static native void saveScreenshot(String name, HTMLCanvasElement cvs);
+
+ public static enum RateLimit {
+ NONE, FAILED, BLOCKED, FAILED_POSSIBLY_LOCKED, LOCKED, NOW_LOCKED;
+ }
+
+ private static final Set rateLimitedAddresses = new HashSet<>();
+ private static final Set blockedAddresses = new HashSet<>();
+
+ private static WebSocket sock = null;
+ private static boolean sockIsConnecting = false;
+ private static boolean sockIsConnected = false;
+ private static boolean sockIsAlive = false;
+ private static LinkedList readPackets = new LinkedList<>();
+ private static RateLimit rateLimitStatus = null;
+ private static String currentSockURI = null;
+
+ public static final RateLimit getRateLimitStatus() {
+ RateLimit l = rateLimitStatus;
+ rateLimitStatus = null;
+ return l;
+ }
+ public static final void logRateLimit(String addr, RateLimit l) {
+ if(l == RateLimit.BLOCKED) {
+ blockedAddresses.add(addr);
+ }else {
+ rateLimitedAddresses.add(addr);
+ }
+ }
+ public static final RateLimit checkRateLimitHistory(String addr) {
+ if(blockedAddresses.contains(addr)) {
+ return RateLimit.LOCKED;
+ }else if(rateLimitedAddresses.contains(addr)) {
+ return RateLimit.BLOCKED;
+ }else {
+ return RateLimit.NONE;
+ }
+ }
+
+ @Async
+ public static native String connectWebSocket(String sockURI);
+
+ private static void connectWebSocket(String sockURI, final AsyncCallback cb) {
+ sockIsConnecting = true;
+ sockIsConnected = false;
+ sockIsAlive = false;
+ rateLimitStatus = null;
+ currentSockURI = sockURI;
+ try {
+ sock = new WebSocket(sockURI);
+ } catch(Throwable t) {
+ sockIsConnecting = false;
+ sockIsAlive = false;
+ return;
+ }
+ sock.setBinaryType("arraybuffer");
+ sock.addEventListener("open", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ sockIsConnecting = false;
+ sockIsAlive = false;
+ sockIsConnected = true;
+ readPackets.clear();
+ cb.complete("okay");
+ }
+ });
+ sock.addEventListener("close", new EventListener() {
+ @Override
+ public void handleEvent(CloseEvent evt) {
+ sock = null;
+ if(sockIsConnecting) {
+ if(rateLimitStatus == null) {
+ if(blockedAddresses.contains(currentSockURI)) {
+ rateLimitStatus = RateLimit.LOCKED;
+ }else if(rateLimitedAddresses.contains(currentSockURI)) {
+ rateLimitStatus = RateLimit.FAILED_POSSIBLY_LOCKED;
+ }else {
+ rateLimitStatus = RateLimit.FAILED;
+ }
+ }
+ }else if(!sockIsAlive) {
+ if(rateLimitStatus == null) {
+ if(blockedAddresses.contains(currentSockURI)) {
+ rateLimitStatus = RateLimit.LOCKED;
+ }else if(rateLimitedAddresses.contains(currentSockURI)) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ }
+ }
+ }
+ boolean b = sockIsConnecting;
+ sockIsConnecting = false;
+ sockIsConnected = false;
+ sockIsAlive = false;
+ if(b) cb.complete("fail");
+ }
+ });
+ sock.addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ sockIsAlive = true;
+ if(isString(evt.getData())) {
+ String stat = evt.getDataAsString();
+ if(stat.equalsIgnoreCase("BLOCKED")) {
+ if(rateLimitStatus == null) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ }
+ rateLimitedAddresses.add(currentSockURI);
+ }else if(stat.equalsIgnoreCase("LOCKED")) {
+ if(rateLimitStatus == null) {
+ rateLimitStatus = RateLimit.NOW_LOCKED;
+ }
+ rateLimitedAddresses.add(currentSockURI);
+ blockedAddresses.add(currentSockURI);
+ }
+ sockIsConnecting = false;
+ sockIsConnected = false;
+ sock.close();
+ return;
+ }
+ readPackets.add(TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray()));
+ }
+ });
+ }
+
+ public static final boolean startConnection(String uri) {
+ String res = connectWebSocket(uri);
+ return !"fail".equals(res);
+ }
+ public static final void endConnection() {
+ if(sock == null || sock.getReadyState() == 3) {
+ sockIsConnecting = false;
+ }
+ if(sock != null && !sockIsConnecting) sock.close();
+ }
+ public static final boolean connectionOpen() {
+ if(IntegratedServer.doesChannelExist(EaglerProfile.username) && IntegratedServer.isWorldRunning()) {
+ return true;
+ }
+ if(!EaglerAdapter.clientLANClosed()) {
+ return true;
+ }
+ if(sock == null || sock.getReadyState() == 3) {
+ sockIsConnecting = false;
+ }
+ return sock != null && !sockIsConnecting && sock.getReadyState() != 3;
+ }
+ @JSBody(params = { "sock", "buffer" }, script = "sock.send(buffer);")
+ private static native void nativeBinarySend(WebSocket sock, ArrayBuffer buffer);
+ public static final void writePacket(byte[] packet) {
+ if(sock != null && !sockIsConnecting) {
+ nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(packet));
+ }
+ }
+ public static final byte[] readPacket() {
+ if(!readPackets.isEmpty()) {
+ return readPackets.remove(0);
+ }else {
+ return null;
+ }
+ }
+ public static final byte[] loadLocalStorage(String key) {
+ try {
+ Storage strg = win.getLocalStorage();
+ if(strg != null) {
+ String s = strg.getItem("_eaglercraft."+key);
+ if(s != null) {
+ return Base64.decodeBase64(s);
+ }else {
+ return null;
+ }
+ }else {
+ return null;
+ }
+ }catch(Throwable t) {
+ return null;
+ }
+ }
+ public static final void saveLocalStorage(String key, byte[] data) {
+ try {
+ Storage strg = win.getLocalStorage();
+ if(strg != null) {
+ strg.setItem("_eaglercraft."+key, Base64.encodeBase64String(data));
+ }
+ }catch(Throwable t) {
+ }
+ }
+ public static final void openLink(String url) {
+ SelfDefence.openWindowIgnore(url, "_blank");
+ }
+ public static final void redirectTo(String url) {
+ win.getLocation().setFullURL(url);
+ }
+
+ @JSBody(params = { }, script = "window.onbeforeunload = function(){javaMethods.get('net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.onWindowUnload()V').invoke();return false;};")
+ private static native void onBeforeCloseRegister();
+
+ public static final void openFileChooser(String ext, String mime) {
+ fileChooser.openFileChooser(ext, mime);
+ }
+
+ public static final boolean getFileChooserResultAvailable() {
+ return fileChooser.getFileChooserResult() != null;
+ }
+
+ public static final byte[] getFileChooserResult() {
+ ArrayBuffer b = getFileChooserResult0();
+ if(b == null) return null;
+ return TeaVMUtils.wrapByteArrayBuffer(b);
+ }
+
+ public static final void clearFileChooserResult() {
+ getFileChooserResult0();
+ }
+
+ private static final ArrayBuffer getFileChooserResult0() {
+ ArrayBuffer ret = fileChooser.getFileChooserResult();
+ fileChooser.setFileChooserResult(null);
+ return ret;
+ }
+
+ public static final String getFileChooserResultName() {
+ return fileChooser.getFileChooserResultName();
+ }
+
+ public static final void setListenerPos(float x, float y, float z, float vx, float vy, float vz, float pitch, float yaw) {
+ float var2 = MathHelper.cos(-yaw * 0.017453292F);
+ float var3 = MathHelper.sin(-yaw * 0.017453292F);
+ float var4 = -MathHelper.cos(pitch * 0.017453292F);
+ float var5 = MathHelper.sin(pitch * 0.017453292F);
+ AudioListener l = audioctx.getListener();
+ l.setPosition(x, y, z);
+ l.setOrientation(-var3 * var4, -var5, -var2 * var4, 0.0f, 1.0f, 0.0f);
+ }
+
+ private static int playbackId = 0;
+ private static int audioElementId = 0;
+ private static final HashMap loadedSoundFiles = new HashMap<>();
+ private static AudioContext audioctx = null;
+ private static GainNode masterVolumeNode = null;
+ private static GainNode musicVolumeNode = null;
+ private static float playbackOffsetDelay = 0.03f;
+
+ public static final void setPlaybackOffsetDelay(float f) {
+ playbackOffsetDelay = f;
+ }
+
+ private static final void setGainlessAudioVolume(float oldGain, float f, boolean music) {
+ if (f != oldGain) {
+ for (AudioSourceNodeX a : activeSoundEffects.values()) {
+ if (a.music == music && a instanceof MediaElementAudioSourceNodeX && a.gain == null) {
+ HTMLAudioElement aud = ((MediaElementAudioSourceNodeX) a).audio;
+ float newVolume = 0.5F;
+ if (oldGain == 0) {
+ aud.setMuted(false);
+ newVolume = f * aud.getVolume();
+ } else if (f == 0) {
+ aud.setMuted(true);
+ newVolume = aud.getVolume() / oldGain;
+ } else {
+ newVolume = f * aud.getVolume() / oldGain;
+ }
+ aud.setVolume(newVolume > 1.0f ? 1.0f : newVolume);
+ }
+ }
+ }
+ }
+
+ public static final void setMasterVolume(float f) {
+ setGainlessAudioVolume(masterVolumeNode.getGain().getValue(), f, false);
+ masterVolumeNode.getGain().setValue(f);
+ }
+
+ public static final void setMusicVolume(float f) {
+ setGainlessAudioVolume(musicVolumeNode.getGain().getValue(), f, true);
+ musicVolumeNode.getGain().setValue(f);
+ }
+
+ @Async
+ public static native AudioBuffer decodeAudioAsync(ArrayBuffer buffer);
+
+ private static void decodeAudioAsync(ArrayBuffer buffer, final AsyncCallback cb) {
+ audioctx.decodeAudioData(buffer, new DecodeSuccessCallback() {
+ @Override
+ public void onSuccess(AudioBuffer decodedData) {
+ cb.complete(decodedData);
+ }
+ }, new DecodeErrorCallback() {
+ @Override
+ public void onError(JSObject error) {
+ cb.complete(null);
+ }
+ });
+ }
+
+ private static final HashMap activeSoundEffects = new HashMap<>();
+
+ private static class AudioBufferX {
+ private final AudioBuffer buffer;
+ private AudioBufferX(AudioBuffer buffer) {
+ this.buffer = buffer;
+ }
+ }
+
+ private static class AudioSourceNodeX {
+ private final PannerNode panner;
+ private final GainNode gain;
+ private final boolean music;
+ private AudioSourceNodeX(PannerNode panner, GainNode gain, boolean music) {
+ this.panner = panner;
+ this.gain = gain;
+ this.music = music;
+ }
+ }
+
+ private static class AudioBufferSourceNodeX extends AudioSourceNodeX {
+ private final AudioBufferSourceNode source;
+ private AudioBufferSourceNodeX(AudioBufferSourceNode source, PannerNode panner, GainNode gain, boolean music) {
+ super(panner, gain, music);
+ this.source = source;
+ }
+ }
+
+ private static class MediaElementAudioSourceNodeX extends AudioSourceNodeX {
+ private final MediaElementAudioSourceNode source;
+ private final HTMLAudioElement audio;
+ private MediaElementAudioSourceNodeX(MediaElementAudioSourceNode source, HTMLAudioElement audio, PannerNode panner, GainNode gain, boolean music) {
+ super(panner, gain, music);
+ this.source = source;
+ this.audio = audio;
+ }
+ }
+
+ @JSBody(params = { "playing", "volume" }, script = "window.dispatchEvent(new CustomEvent('eagTitleMusic', { detail: { playing: playing, volume: volume } }));return;")
+ public static native void fireTitleMusicEvent(boolean playing, float volume);
+
+ private static final AudioBuffer getBufferFor(String fileName) {
+ AudioBufferX ret = loadedSoundFiles.get(fileName);
+ if(ret == null) {
+ byte[] file = loadResourceBytes(fileName);
+ if(file == null) return null;
+ Uint8Array buf = new Uint8Array(file.length);
+ buf.set(file);
+ ret = new AudioBufferX(decodeAudioAsync(buf.getBuffer()));
+ loadedSoundFiles.put(fileName, ret);
+ }
+ return ret.buffer;
+ }
+ public static final int beginPlayback(String fileName, float x, float y, float z, float volume, float pitch) {
+ return beginPlayback(fileName, x, y, z, volume, pitch, false);
+ }
+ public static final int beginPlayback(String fileNamePre, float x, float y, float z, float volume, float pitch, boolean music) {
+ if(fileNamePre.startsWith("/")) fileNamePre = fileNamePre.substring(1);
+ String fileName = AssetRepository.fileNameOverrides.getOrDefault(fileNamePre, fileNamePre);
+ AudioNode s;
+ HTMLAudioElement audioElement = null;
+ String lowerFileName = fileName.toLowerCase();
+ boolean usingUrl = AssetRepository.fileNameOverrides.containsKey(fileNamePre) || lowerFileName.startsWith("http://") || lowerFileName.startsWith("https://") || lowerFileName.startsWith("blob:") || lowerFileName.startsWith("data:");
+ if (usingUrl) {
+ audioElement = (HTMLAudioElement) win.getDocument().createElement("audio");
+ audioElement.setAutoplay(true);
+ audioElement.setCrossOrigin("anonymous");
+ audioElement.setSrc(fileName);
+ s = audioctx.createMediaElementSource(audioElement);
+ audioElement.setPlaybackRate(pitch);
+ } else {
+ AudioBuffer b = getBufferFor(fileName);
+ if(b == null) return -1;
+ s = audioctx.createBufferSource();
+ ((AudioBufferSourceNode) s).setBuffer(b);
+ ((AudioBufferSourceNode) s).getPlaybackRate().setValue(pitch);
+ }
+ ChannelMergerNode c = audioctx.createChannelMerger(1);
+ PannerNode p = audioctx.createPanner();
+ p.setPosition(x, y, z);
+ p.setMaxDistance(volume * 16f + 0.1f);
+ p.setRolloffFactor(1f);
+ //p.setVelocity(0f, 0f, 0f);
+ p.setDistanceModel("linear");
+ p.setPanningModel("HRTF");
+ p.setConeInnerAngle(360f);
+ p.setConeOuterAngle(0f);
+ p.setConeOuterGain(0f);
+ p.setOrientation(0f, 1f, 0f);
+ GainNode g = audioctx.createGain();
+ g.getGain().setValue(volume > 1.0f ? 1.0f : volume);
+ s.connect(c);
+ c.connect(g);
+ g.connect(p);
+ p.connect(music ? musicVolumeNode : masterVolumeNode);
+ if (!usingUrl) {
+ ((AudioBufferSourceNode) s).start(0.0d, playbackOffsetDelay);
+ }
+ final int theId = ++playbackId;
+ if (usingUrl) {
+ activeSoundEffects.put(theId, new MediaElementAudioSourceNodeX((MediaElementAudioSourceNode) s, audioElement, p, g, music));
+ audioElement.addEventListener("canplay", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ if (activeSoundEffects.containsKey(theId)) {
+ ((MediaElementAudioSourceNodeX) activeSoundEffects.get(theId)).audio.play();
+ }
+ }
+ });
+ audioElement.addEventListener("ended", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ ((MediaElementAudioSourceNodeX) activeSoundEffects.remove(theId)).audio.setSrc("");
+ }
+ });
+ } else {
+ activeSoundEffects.put(theId, new AudioBufferSourceNodeX((AudioBufferSourceNode) s, p, g, music));
+ ((AudioBufferSourceNode) s).setOnEnded(new EventListener() {
+ @Override
+ public void handleEvent(MediaEvent evt) {
+ activeSoundEffects.remove(theId);
+ }
+ });
+ }
+ return theId;
+ }
+ public static final int beginPlaybackStatic(String fileName, float volume, float pitch) {
+ return beginPlaybackStatic(fileName, volume, pitch, false);
+ }
+ public static final int beginPlaybackStatic(String fileNamePre, float volume, float pitch, boolean music) {
+ if(fileNamePre.startsWith("/")) fileNamePre = fileNamePre.substring(1);
+ String fileName = AssetRepository.fileNameOverrides.getOrDefault(fileNamePre, fileNamePre);
+ AudioNode s = null;
+ GainNode g = null;
+ HTMLAudioElement audioElement = null;
+ String lowerFileName = fileName.toLowerCase();
+ boolean usingUrl = AssetRepository.fileNameOverrides.containsKey(fileNamePre) || lowerFileName.startsWith("http://") || lowerFileName.startsWith("https://") || lowerFileName.startsWith("blob:") || lowerFileName.startsWith("data:");
+ if (usingUrl) {
+ audioElement = (HTMLAudioElement) win.getDocument().createElement("audio");
+ audioElement.setAutoplay(true);
+ // audioElement.setCrossOrigin("anonymous");
+ audioElement.setSrc(fileName);
+ audioElement.setPlaybackRate(pitch);
+ } else {
+ AudioBuffer b = getBufferFor(fileName);
+ if(b == null) return -1;
+ s = audioctx.createBufferSource();
+ ((AudioBufferSourceNode) s).setBuffer(b);
+ ((AudioBufferSourceNode) s).getPlaybackRate().setValue(pitch);
+ g = audioctx.createGain();
+ g.getGain().setValue(volume > 1.0f ? 1.0f : volume);
+ s.connect(g);
+ g.connect(music ? musicVolumeNode : masterVolumeNode);
+ ((AudioBufferSourceNode) s).start(0.0d, playbackOffsetDelay);
+ }
+
+ final int theId = ++playbackId;
+ if (usingUrl) {
+ activeSoundEffects.put(theId, new MediaElementAudioSourceNodeX(null, audioElement, null, null, music));
+ audioElement.addEventListener("canplay", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ if (activeSoundEffects.containsKey(theId)) {
+ ((MediaElementAudioSourceNodeX) activeSoundEffects.get(theId)).audio.play();
+ }
+ }
+ });
+ audioElement.addEventListener("ended", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ ((MediaElementAudioSourceNodeX) activeSoundEffects.remove(theId)).audio.setSrc("");
+ }
+ });
+ } else {
+ activeSoundEffects.put(theId, new AudioBufferSourceNodeX(((AudioBufferSourceNode) s), null, g, music));
+ ((AudioBufferSourceNode) s).setOnEnded(new EventListener() {
+ @Override
+ public void handleEvent(MediaEvent evt) {
+ activeSoundEffects.remove(theId);
+ }
+ });
+ }
+ return theId;
+ }
+ public static final void setPitch(int id, float pitch) {
+ AudioSourceNodeX a = activeSoundEffects.get(id);
+ if(a != null) {
+ if (a instanceof AudioBufferSourceNodeX) {
+ ((AudioBufferSourceNodeX) a).source.getPlaybackRate().setValue(pitch);
+ } else if (a instanceof MediaElementAudioSourceNodeX) {
+ ((MediaElementAudioSourceNodeX) a).audio.setPlaybackRate(pitch);
+ }
+ }
+ }
+ public static final void setVolume(int id, float volume) {
+ AudioSourceNodeX a = activeSoundEffects.get(id);
+ if(a != null) {
+ if (a instanceof MediaElementAudioSourceNodeX && a.gain == null) {
+ HTMLAudioElement audioElem = ((MediaElementAudioSourceNodeX) a).audio;
+ float gainValue = (a.music ? musicVolumeNode : masterVolumeNode).getGain().getValue();
+ float newVolume;
+ if (gainValue == 0) {
+ audioElem.setMuted(true);
+ newVolume = volume;
+ } else {
+ audioElem.setMuted(false);
+ newVolume = gainValue * volume;
+ }
+ audioElem.setVolume(newVolume > 1.0f ? 1.0f : volume);
+ } else {
+ a.gain.getGain().setValue(volume > 1.0f ? 1.0f : volume);
+ if (a.panner != null) a.panner.setMaxDistance(volume * 16f + 0.1f);
+ }
+ }
+ }
+ public static final void moveSound(int id, float x, float y, float z, float vx, float vy, float vz) {
+ AudioSourceNodeX a = activeSoundEffects.get(id);
+ if(a != null && a.panner != null) {
+ a.panner.setPosition(x, y, z);
+ //a.panner.setVelocity(vx, vy, vz);
+ }
+ }
+ public static final void endSound(int id) {
+ AudioSourceNodeX a = activeSoundEffects.get(id);
+ if(a != null) {
+ if (a instanceof AudioBufferSourceNodeX) {
+ ((AudioBufferSourceNodeX) a).source.stop();
+ } else if (a instanceof MediaElementAudioSourceNodeX) {
+ ((MediaElementAudioSourceNodeX) a).audio.pause();
+ ((MediaElementAudioSourceNodeX) a).audio.setSrc("");
+ }
+ activeSoundEffects.remove(id);
+ }
+ }
+ public static final boolean isPlaying(int id) {
+ return activeSoundEffects.containsKey(id);
+ }
+ public static final void openConsole() {
+ Window.alert("Still under development");
+ }
+
+ private static EaglercraftVoiceClient voiceClient = null;
+
+ private static boolean voiceAvailableStat = false;
+ private static boolean voiceSignalHandlersInitialized = false;
+
+ private static Consumer returnSignalHandler = null;
+
+ private static final HashMap voiceAnalysers = new HashMap<>();
+ private static final HashMap voiceGains = new HashMap<>();
+ private static final HashMap voicePanners = new HashMap<>();
+ private static final HashSet nearbyPlayers = new HashSet<>();
+
+ public static void clearVoiceAvailableStatus() {
+ voiceAvailableStat = false;
+ }
+
+ public static void setVoiceSignalHandler(Consumer signalHandler) {
+ returnSignalHandler = signalHandler;
+ }
+
+ public static final int VOICE_SIGNAL_ALLOWED = 0;
+ public static final int VOICE_SIGNAL_REQUEST = 0;
+ public static final int VOICE_SIGNAL_CONNECT = 1;
+ public static final int VOICE_SIGNAL_DISCONNECT = 2;
+ public static final int VOICE_SIGNAL_ICE = 3;
+ public static final int VOICE_SIGNAL_DESC = 4;
+ public static final int VOICE_SIGNAL_GLOBAL = 5;
+
+ public static void handleVoiceSignal(byte[] data) {
+ try {
+ DataInputStream streamIn = new DataInputStream(new EaglerInputStream(data));
+ int sig = streamIn.read();
+ switch(sig) {
+ case VOICE_SIGNAL_GLOBAL:
+ String[] voicePlayers = new String[streamIn.readInt()];
+ for(int i = 0; i < voicePlayers.length; i++) voicePlayers[i] = streamIn.readUTF();
+ for (String username : voicePlayers) {
+ // notice that literally everyone except for those already connected using voice chat will receive the request; however, ones using proximity will simply ignore it.
+ sendVoiceRequestIfNeeded(username);
+ }
+ break;
+ case VOICE_SIGNAL_ALLOWED:
+ voiceAvailableStat = streamIn.read() == 1;
+ String[] servs = new String[streamIn.read()];
+ for(int i = 0; i < servs.length; i++) {
+ servs[i] = streamIn.readUTF();
+ }
+ voiceClient.setICEServers(servs);
+ break;
+ case VOICE_SIGNAL_CONNECT:
+ String peerId = streamIn.readUTF();
+ try {
+ boolean offer = streamIn.readBoolean();
+ voiceClient.signalConnect(peerId, offer);
+ } catch (EOFException e) { // this is actually a connect ANNOUNCE, not an absolute "yes please connect" situation
+ if (false && !nearbyPlayers.contains(peerId)) return;
+ // send request to peerId
+ sendVoiceRequest(peerId);
+ }
+ break;
+ case VOICE_SIGNAL_DISCONNECT:
+ String peerId2 = streamIn.readUTF();
+ voiceClient.signalDisconnect(peerId2, true);
+ break;
+ case VOICE_SIGNAL_ICE:
+ String peerId3 = streamIn.readUTF();
+ String candidate = streamIn.readUTF();
+ voiceClient.signalICECandidate(peerId3, candidate);
+ break;
+ case VOICE_SIGNAL_DESC:
+ String peerId4 = streamIn.readUTF();
+ String descJSON = streamIn.readUTF();
+ voiceClient.signalDescription(peerId4, descJSON);
+ break;
+ default:
+ System.err.println("Unknown voice signal packet '" + sig + "'!");
+ break;
+ }
+ }catch(IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ public static final boolean voiceAvailable() {
+ return voiceClient.voiceClientSupported() && voiceClient.getReadyState() != EaglercraftVoiceClient.READYSTATE_ABORTED;
+ }
+ public static final boolean voiceAllowed() {
+ return voiceAvailableStat;
+ }
+ public static final boolean voiceRelayed() {
+ return false;
+ }
+
+ public static final void addNearbyPlayer(String username) {
+ recentlyNearbyPlayers.remove(username);
+ if (nearbyPlayers.add(username)) {
+ sendVoiceRequestIfNeeded(username);
+ }
+ }
+
+ private static final void sendVoiceRequest(String username) {
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+ dos.write(VOICE_SIGNAL_REQUEST);
+ dos.writeUTF(username);
+ returnSignalHandler.accept(baos.toByteArray());
+ } catch (IOException ignored) { }
+ }
+
+ private static final void sendVoiceRequestIfNeeded(String username) {
+
+ }
+
+ private static final ExpiringSet recentlyNearbyPlayers = new ExpiringSet<>(5000, new ExpiringSet.ExpiringEvent() {
+ @Override
+ public void onExpiration(String username) {
+ if (!nearbyPlayers.contains(username)) voiceClient.signalDisconnect(username, false);
+ }
+ });
+
+ public static final void removeNearbyPlayer(String username) {
+
+ }
+
+ public static final void cleanupNearbyPlayers(HashSet existingPlayers) {
+ nearbyPlayers.stream().filter(un -> !existingPlayers.contains(un)).collect(Collectors.toSet()).forEach(EaglerAdapterImpl2::removeNearbyPlayer);
+ }
+
+ public static final void updateVoicePosition(String username, double x, double y, double z) {
+ if (voicePanners.containsKey(username)) voicePanners.get(username).setPosition((float) x, (float) y, (float) z);
+ }
+
+ public static final void sendInitialVoice() {
+ returnSignalHandler.accept(new byte[] { VOICE_SIGNAL_CONNECT });
+ for (String username : nearbyPlayers) sendVoiceRequest(username);
+ }
+
+ private static boolean talkStatus = false;
+ public static final void activateVoice(boolean talk) {
+ if(talkStatus != talk) {
+ voiceClient.activateVoice(talk);
+ }
+ talkStatus = talk;
+ }
+
+ private static int proximity = 16;
+ public static final void setVoiceProximity(int prox) {
+ for (PannerNode panner : voicePanners.values()) panner.setMaxDistance(getVoiceListenVolume() * 2 * prox + 0.1f);
+ proximity = prox;
+ }
+ public static final int getVoiceProximity() {
+ return proximity;
+ }
+
+ private static float volumeListen = 0.5f;
+ public static final void setVoiceListenVolume(float f) {
+ for (String username : voiceGains.keySet()) {
+ GainNode gain = voiceGains.get(username);
+ float val = f;
+ if(val > 0.5f) val = 0.5f + (val - 0.5f) * 3.0f;
+ if(val > 2.0f) val = 2.0f;
+ if(val < 0.0f) val = 0.0f;
+ gain.getGain().setValue(val * 2.0f);
+ if (voicePanners.containsKey(username)) voicePanners.get(username).setMaxDistance(f * 2 * getVoiceProximity() + 0.1f);
+ }
+ volumeListen = f;
+ }
+ public static final float getVoiceListenVolume() {
+ return volumeListen;
+ }
+
+ private static float volumeSpeak = 0.5f;
+ public static final void setVoiceSpeakVolume(float f) {
+ if(volumeSpeak != f) {
+ voiceClient.setMicVolume(f);
+ }
+ volumeSpeak = f;
+ }
+ public static final float getVoiceSpeakVolume() {
+ return volumeSpeak;
+ }
+
+ private static final Set mutedSet = new HashSet<>();
+ private static final Set speakingSet = new HashSet<>();
+ public static final Set getVoiceListening() {
+ return voiceGains.keySet();
+ }
+ public static final Set getVoiceSpeaking() {
+ return speakingSet;
+ }
+ public static final void setVoiceMuted(String username, boolean mute) {
+ voiceClient.mutePeer(username, mute);
+ if(mute) {
+ mutedSet.add(username);
+ }else {
+ mutedSet.remove(username);
+ }
+ }
+ public static final Set getVoiceMuted() {
+ return mutedSet;
+ }
+ public static final List getVoiceRecent() {
+ return new ArrayList<>(voiceGains.keySet());
+ }
+
+ public static final void tickVoice() {
+ recentlyNearbyPlayers.checkForExpirations();
+ speakingSet.clear();
+ for (String username : voiceAnalysers.keySet()) {
+ AnalyserNode analyser = voiceAnalysers.get(username);
+ Uint8Array array = new Uint8Array(analyser.getFrequencyBinCount());
+ analyser.getByteFrequencyData(array);
+ int len = array.getLength();
+ for (int i = 0; i < len; i++) {
+ if (array.get(i) >= 0.1f) {
+ speakingSet.add(username);
+ break;
+ }
+ }
+ }
+ }
+
+
+ public static final void doJavascriptCoroutines() {
+
+ }
+ public static final long maxMemory() {
+ return 1024*1024*1024;
+ }
+ public static final long totalMemory() {
+ return 1024*1024*1024;
+ }
+ public static final long freeMemory() {
+ return 0l;
+ }
+ public static final void exit() {
+
+ }
+
+ @JSBody(params = { }, script = "return window.navigator.userAgent;")
+ public static native String getUserAgent();
+
+ private static String[] LWJGLKeyNames = new String[] {"NONE", "ESCAPE", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "MINUS", "EQUALS", "BACK", "TAB", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "LBRACKET", "RBRACKET", "RETURN", "LCONTROL", "A", "S", "D", "F", "G", "H", "J", "K", "L", "SEMICOLON", "APOSTROPHE", "GRAVE", "LSHIFT", "BACKSLASH", "Z", "X", "C", "V", "B", "N", "M", "COMMA", "PERIOD", "SLASH", "RSHIFT", "MULTIPLY", "LMENU", "SPACE", "CAPITAL", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "NUMLOCK", "SCROLL", "NUMPAD7", "NUMPAD8", "NUMPAD9", "SUBTRACT", "NUMPAD4", "NUMPAD5", "NUMPAD6", "ADD", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD0", "DECIMAL", "null", "null", "null", "F11", "F12", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "F13", "F14", "F15", "F16", "F17", "F18", "null", "null", "null", "null", "null", "null", "KANA", "F19", "null", "null", "null", "null", "null", "null", "null", "CONVERT", "null", "NOCONVERT", "null", "YEN", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "NUMPADEQUALS", "null", "null", "CIRCUMFLEX", "AT", "COLON", "UNDERLINE", "KANJI", "STOP", "AX", "UNLABELED", "null", "null", "null", "null", "NUMPADENTER", "RCONTROL", "null", "null", "null", "null", "null", "null", "null", "null", "null", "SECTION", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "NUMPADCOMMA", "null", "DIVIDE", "null", "SYSRQ", "RMENU", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "FUNCTION", "PAUSE", "null", "HOME", "UP", "PRIOR", "null", "LEFT", "null", "RIGHT", "null", "END", "DOWN", "NEXT", "INSERT", "DELETE", "null", "null", "null", "null", "null", "null", "CLEAR", "LMETA", "RMETA", "APPS", "POWER", "SLEEP", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null", "null"};
+
+ private static int[] LWJGLKeyCodes = new int[] {
+ /* 0 */ -1, /* 1 */ -1, /* 2 */ -1, /* 3 */ -1, /* 4 */ -1,
+ /* 5 */ -1, /* 6 */ -1, /* 7 */ -1, /* 8 */ 14, /* 9 */ 15,
+ /* 10 */ -1, /* 11 */ -1, /* 12 */ -1, /* 13 */ 28, /* 14 */ -1,
+ /* 15 */ -1, /* 16 */ 42, /* 17 */ 29, /* 18 */ 56, /* 19 */ -1,
+ /* 20 */ -1, /* 21 */ -1, /* 22 */ -1, /* 23 */ -1, /* 24 */ -1,
+ /* 25 */ -1, /* 26 */ -1, /* 27 */ 1, /* 28 */ -1, /* 29 */ -1,
+ /* 30 */ -1, /* 31 */ -1, /* 32 */ 57, /* 33 */ 210, /* 34 */ 201,
+ /* 35 */ 207, /* 36 */ 199, /* 37 */ 203, /* 38 */ 200, /* 39 */ 205,
+ /* 40 */ 208, /* 41 */ 205, /* 42 */ 208, /* 43 */ -1, /* 44 */ -1,
+ /* 45 */ 210, /* 46 */ 211, /* 47 */ 211, /* 48 */ 11, /* 49 */ 2,
+ /* 50 */ 3, /* 51 */ 4, /* 52 */ 5, /* 53 */ 6, /* 54 */ 7,
+ /* 55 */ 8, /* 56 */ 9, /* 57 */ 10, /* 58 */ -1, /* 59 */ -1,
+ /* 60 */ -1, /* 61 */ -1, /* 62 */ -1, /* 63 */ -1, /* 64 */ -1,
+ /* 65 */ 30, /* 66 */ 48, /* 67 */ 46, /* 68 */ 32, /* 69 */ 18,
+ /* 70 */ 33, /* 71 */ 34, /* 72 */ 35, /* 73 */ 23, /* 74 */ 36,
+ /* 75 */ 37, /* 76 */ 38, /* 77 */ 50, /* 78 */ 49, /* 79 */ 24,
+ /* 80 */ 25, /* 81 */ 16, /* 82 */ 19, /* 83 */ 31, /* 84 */ 20,
+ /* 85 */ 22, /* 86 */ 47, /* 87 */ 17, /* 88 */ 45, /* 89 */ 21,
+ /* 90 */ 44, /* 91 */ -1, /* 92 */ -1, /* 93 */ -1, /* 94 */ -1,
+ /* 95 */ -1, /* 96 */ -1, /* 97 */ -1, /* 98 */ -1, /* 99 */ -1,
+ /* 100 */ -1, /* 101 */ -1, /* 102 */ -1, /* 103 */ -1, /* 104 */ -1,
+ /* 105 */ -1, /* 106 */ -1, /* 107 */ -1, /* 108 */ -1, /* 109 */ 12,
+ /* 110 */ 52, /* 111 */ 53, /* 112 */ -1, /* 113 */ -1, /* 114 */ -1,
+ /* 115 */ -1, /* 116 */ -1, /* 117 */ -1, /* 118 */ -1, /* 119 */ -1,
+ /* 120 */ -1, /* 121 */ -1, /* 122 */ -1, /* 123 */ -1, /* 124 */ -1,
+ /* 125 */ -1, /* 126 */ -1, /* 127 */ -1, /* 128 */ -1, /* 129 */ -1,
+ /* 130 */ -1, /* 131 */ -1, /* 132 */ -1, /* 133 */ -1, /* 134 */ -1,
+ /* 135 */ -1, /* 136 */ -1, /* 137 */ -1, /* 138 */ -1, /* 139 */ -1,
+ /* 140 */ -1, /* 141 */ -1, /* 142 */ -1, /* 143 */ -1, /* 144 */ -1,
+ /* 145 */ -1, /* 146 */ -1, /* 147 */ -1, /* 148 */ -1, /* 149 */ -1,
+ /* 150 */ -1, /* 151 */ -1, /* 152 */ -1, /* 153 */ -1, /* 154 */ -1,
+ /* 155 */ -1, /* 156 */ -1, /* 157 */ -1, /* 158 */ -1, /* 159 */ -1,
+ /* 160 */ -1, /* 161 */ -1, /* 162 */ -1, /* 163 */ -1, /* 164 */ -1,
+ /* 165 */ -1, /* 166 */ -1, /* 167 */ -1, /* 168 */ -1, /* 169 */ -1,
+ /* 170 */ -1, /* 171 */ -1, /* 172 */ -1, /* 173 */ -1, /* 174 */ -1,
+ /* 175 */ -1, /* 176 */ -1, /* 177 */ -1, /* 178 */ -1, /* 179 */ -1,
+ /* 180 */ -1, /* 181 */ -1, /* 182 */ -1, /* 183 */ -1, /* 184 */ -1,
+ /* 185 */ -1, /* 186 */ 39, /* 187 */ 13, /* 188 */ 51, /* 189 */ 12,
+ /* 190 */ 52, /* 191 */ 53, /* 192 */ -1, /* 193 */ -1, /* 194 */ -1,
+ /* 195 */ -1, /* 196 */ -1, /* 197 */ -1, /* 198 */ -1, /* 199 */ -1,
+ /* 200 */ -1, /* 201 */ -1, /* 202 */ -1, /* 203 */ -1, /* 204 */ -1,
+ /* 205 */ -1, /* 206 */ -1, /* 207 */ -1, /* 208 */ -1, /* 209 */ -1,
+ /* 210 */ -1, /* 211 */ -1, /* 212 */ -1, /* 213 */ -1, /* 214 */ -1,
+ /* 215 */ -1, /* 216 */ -1, /* 217 */ -1, /* 218 */ -1, /* 219 */ 26,
+ /* 220 */ 43, /* 221 */ 27, /* 222 */ 40
+ };
+
+ public static final int _wArrayByteLength(Object obj) {
+ return ((Int32Array)obj).getByteLength();
+ }
+
+ public static final Object _wCreateLowLevelIntBuffer(int len) {
+ return new Int32Array(len);
+ }
+
+ private static int appendbufferindex = 0;
+ private static Int32Array appendbuffer = new Int32Array(new ArrayBuffer(525000*4));
+
+ public static final void _wAppendLowLevelBuffer(Object arr) {
+ Int32Array a = ((Int32Array)arr);
+ if(appendbufferindex + a.getLength() < appendbuffer.getLength()) {
+ appendbuffer.set(a, appendbufferindex);
+ appendbufferindex += a.getLength();
+ }
+ }
+
+ public static final Object _wGetLowLevelBuffersAppended() {
+ Int32Array ret = new Int32Array(appendbuffer.getBuffer(), 0, appendbufferindex);
+ appendbufferindex = 0;
+ return ret;
+ }
+
+ private static int remapKey(int k) {
+ return (k > LWJGLKeyCodes.length || k < 0) ? -1 : LWJGLKeyCodes[k];
+ }
+
+ public static final boolean isIntegratedServerAvailable() {
+ return integratedServerScript != null;
+ }
+
+ @JSFunctor
+ private static interface WorkerBinaryPacketHandler extends JSObject {
+ public void onMessage(String channel, ArrayBuffer buf);
+ }
+
+ private static final HashMap> workerMessageQueue = new HashMap<>();
+
+ private static Worker server = null;
+ private static boolean serverAlive = false;
+
+ private static class WorkerBinaryPacketHandlerImpl implements WorkerBinaryPacketHandler {
+
+ public void onMessage(String channel, ArrayBuffer buf) {
+ if(channel == null) {
+ System.err.println("Recieved IPC packet with null channel");
+ return;
+ }
+
+ serverAlive = true;
+ synchronized(workerMessageQueue) {
+ List existingQueue = workerMessageQueue.get(channel);
+
+ if(existingQueue == null) {
+ System.err.println("Recieved IPC packet with unknown '" + channel + "' channel");
+ return;
+ }
+
+ if(buf == null) {
+ System.err.println("Recieved IPC packet with null buffer");
+ return;
+ }
+
+ existingQueue.add(new PKT(channel, TeaVMUtils.wrapByteArrayBuffer(buf)));
+ }
+ }
+
+ }
+
+ @JSBody(params = { "w", "wb" }, script = "w.onmessage = function(o) { wb(o.data.ch, o.data.dat); };")
+ private static native void registerPacketHandler(Worker w, WorkerBinaryPacketHandler wb);
+
+ @JSBody(params = { "w", "ch", "dat" }, script = "w.postMessage({ ch: ch, dat : dat });")
+ private static native void sendWorkerPacket(Worker w, String channel, ArrayBuffer arr);
+
+ @JSBody(params = { "w", "dbName" }, script = "w.postMessage({ worldDatabaseName : dbName });")
+ private static native void sendWorkerStartPacket(Worker w, String dbName);
+
+ private static String worldDatabaseName = "MAIN";
+
+ public static final void beginLoadingIntegratedServer() {
+ if(server != null) {
+ server.terminate();
+ }
+ workerMessageQueue.put("IPC", new LinkedList());
+ server = new Worker(integratedServerScript);
+ server.onError(new EventListener() {
+ @Override
+ public void handleEvent(ErrorEvent evt) {
+ System.err.println("Worker Error: " + evt.getError());
+ }
+ });
+ sendWorkerStartPacket(server, worldDatabaseName);
+ registerPacketHandler(server, new WorkerBinaryPacketHandlerImpl());
+ }
+
+ public static final void setWorldDatabaseName(String name) {
+ worldDatabaseName = name;
+ }
+
+ public static final boolean isIntegratedServerAlive() {
+ return serverAlive && server != null;
+ }
+
+ public static final void terminateIntegratedServer() {
+ if(server != null) {
+ server.terminate();
+ server = null;
+ serverAlive = false;
+ }
+ }
+
+ public static final void sendToIntegratedServer(String channel, byte[] pkt) {
+ sendWorkerPacket(server, channel, TeaVMUtils.unwrapArrayBuffer(pkt));
+ //System.out.println("[Client][WRITE][" + channel + "]: " + pkt.length);
+ }
+
+ public static final void enableChannel(String channel) {
+ synchronized(workerMessageQueue) {
+ if(workerMessageQueue.containsKey(channel)) {
+ System.err.println("Tried to enable existing channel '" + channel + "' again");
+ }else {
+ System.out.println("[Client][ENABLE][" + channel + "]");
+ workerMessageQueue.put(channel, new LinkedList<>());
+ }
+ }
+ }
+
+ public static final void disableChannel(String channel) {
+ synchronized(workerMessageQueue) {
+ if(workerMessageQueue.remove(channel) == null) {
+ System.err.println("Tried to disable unknown channel '" + channel + "'");
+ }
+ System.out.println("[Client][DISABLE][" + channel + "]");
+ }
+ }
+
+ public static final PKT recieveFromIntegratedServer(String channel) {
+ synchronized(workerMessageQueue) {
+ List list = workerMessageQueue.get(channel);
+ if(list == null) {
+ System.err.println("Tried to read from unknown channel '" + channel + "'");
+ return null;
+ }else {
+ return list.size() > 0 ? list.remove(0) : null;
+ }
+ }
+ }
+
+ @JSBody(params = { "name", "buf" }, script =
+ "var hr = window.URL.createObjectURL(new Blob([buf], {type: \"octet/stream\"}));" +
+ "var a = document.createElement(\"a\");" +
+ "a.href = hr; a.download = name; a.click();" +
+ "window.URL.revokeObjectURL(hr);")
+ private static final native void downloadBytesImpl(String str, ArrayBuffer buf);
+
+ public static final void downloadBytes(String str, byte[] dat) {
+ downloadBytesImpl(str, TeaVMUtils.unwrapArrayBuffer(dat));
+ }
+
+ @JSFunctor
+ private static interface StupidFunctionResolveString extends JSObject {
+ void resolveStr(String s);
+ }
+
+ private static boolean unpressCTRL = false;
+
+ @Async
+ public static native String getClipboard();
+
+ private static void getClipboard(final AsyncCallback cb) {
+ final long start = steadyTimeMillis();
+ getClipboard0(new StupidFunctionResolveString() {
+ @Override
+ public void resolveStr(String s) {
+ if(steadyTimeMillis() - start > 500l) {
+ unpressCTRL = true;
+ }
+ cb.complete(s);
+ }
+ });
+ }
+
+ @JSBody(params = { "cb" }, script = "if(!window.navigator.clipboard) cb(null); else window.navigator.clipboard.readText().then(function(s) { cb(s); }, function(s) { cb(null); });")
+ private static native void getClipboard0(StupidFunctionResolveString cb);
+
+ @JSBody(params = { "str" }, script = "if(window.navigator.clipboard) window.navigator.clipboard.writeText(str);")
+ public static native void setClipboard(String str);
+
+ @JSBody(params = { "obj" }, script = "return typeof obj === \"string\";")
+ private static native boolean isString(JSObject obj);
+
+ private static class ServerQueryImpl implements ServerQuery {
+
+ private final LinkedList queryResponses = new LinkedList<>();
+ private final LinkedList queryResponsesBytes = new LinkedList<>();
+ private final String type;
+ private boolean open;
+ private boolean alive;
+ private String uriString;
+ private long pingStart;
+ private long pingTimer;
+
+ private final WebSocket sock;
+
+ private ServerQueryImpl(String type_, String uri) {
+ type = type_;
+ uriString = uri;
+ alive = false;
+ pingStart = -1l;
+ pingTimer = -1l;
+ WebSocket s = null;
+ try {
+ s = new WebSocket(uri);
+ s.setBinaryType("arraybuffer");
+ open = true;
+ }catch(Throwable t) {
+ open = false;
+ if(EaglerAdapterImpl2.blockedAddresses.contains(uriString)) {
+ queryResponses.add(new QueryResponse(true, -1l));
+ }else if(EaglerAdapterImpl2.rateLimitedAddresses.contains(uriString)) {
+ queryResponses.add(new QueryResponse(false, -1l));
+ }
+ sock = null;
+ return;
+ }
+ sock = s;
+ if(open) {
+ sock.addEventListener("open", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ pingStart = steadyTimeMillis();
+ sock.send("Accept: " + type);
+ }
+ });
+ sock.addEventListener("close", new EventListener() {
+ @Override
+ public void handleEvent(CloseEvent evt) {
+ open = false;
+ if(!alive) {
+ if(EaglerAdapterImpl2.blockedAddresses.contains(uriString)) {
+ queryResponses.add(new QueryResponse(true, pingTimer));
+ }else if(EaglerAdapterImpl2.rateLimitedAddresses.contains(uriString)) {
+ queryResponses.add(new QueryResponse(false, pingTimer));
+ }
+ }
+ }
+ });
+ sock.addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ alive = true;
+ if(pingTimer == -1) {
+ pingTimer = steadyTimeMillis() - pingStart;
+ }
+ if(isString(evt.getData())) {
+ try {
+ String str = evt.getDataAsString();
+ if(str.equalsIgnoreCase("BLOCKED")) {
+ EaglerAdapterImpl2.rateLimitedAddresses.add(uriString);
+ queryResponses.add(new QueryResponse(false, pingTimer));
+ sock.close();
+ return;
+ }else if(str.equalsIgnoreCase("LOCKED")) {
+ EaglerAdapterImpl2.blockedAddresses.add(uriString);
+ queryResponses.add(new QueryResponse(true, pingTimer));
+ sock.close();
+ return;
+ }else {
+ QueryResponse q = new QueryResponse(new JSONObject(str), pingTimer);
+ if(q.rateLimitStatus != null) {
+ if(q.rateLimitStatus == RateLimit.BLOCKED) {
+ EaglerAdapterImpl2.rateLimitedAddresses.add(uriString);
+ }else if(q.rateLimitStatus == RateLimit.LOCKED) {
+ EaglerAdapterImpl2.blockedAddresses.add(uriString);
+ }
+ sock.close();
+ }
+ queryResponses.add(q);
+ }
+ }catch(Throwable t) {
+ System.err.println("Query response could not be parsed: " + t.toString());
+ }
+ }else {
+ queryResponsesBytes.add(TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray()));
+ }
+ }
+ });
+ Window.setTimeout(new TimerHandler() {
+ @Override
+ public void onTimer() {
+ if(open && sock.getReadyState() != 1) {
+ if(sock.getReadyState() == 0) {
+ sock.close();
+ }
+ open = false;
+ }
+ }
+ }, 5000l);
+ }
+ }
+
+ @Override
+ public boolean isQueryOpen() {
+ return open;
+ }
+
+ @Override
+ public void close() {
+ open = false;
+ sock.close();
+ }
+
+ @Override
+ public void send(String str) {
+ sock.send(str);
+ }
+
+ @Override
+ public int responseAvailable() {
+ return queryResponses.size();
+ }
+
+ @Override
+ public int responseBinaryAvailable() {
+ return queryResponsesBytes.size();
+ }
+
+ @Override
+ public QueryResponse getResponse() {
+ return queryResponses.size() > 0 ? queryResponses.remove(0) : null;
+ }
+
+ @Override
+ public byte[] getBinaryResponse() {
+ return queryResponsesBytes.size() > 0 ? queryResponsesBytes.remove(0) : null;
+ }
+
+ }
+
+ public static final ServerQuery openQuery(String type, String uri) {
+ return new ServerQueryImpl(type, uri);
+ }
+
+ private static String serverToJoinOnLaunch = null;
+
+ public static final void setServerToJoinOnLaunch(String s) {
+ serverToJoinOnLaunch = s;
+ }
+
+ public static final String getServerToJoinOnLaunch() {
+ return serverToJoinOnLaunch;
+ }
+
+ private static boolean endianWasChecked = false;
+ private static boolean isBigEndian = false;
+ private static boolean isLittleEndian = false;
+
+ public static final boolean isBigEndian() {
+ if(!endianWasChecked) {
+ int checkIntegerA = 0xFF000000;
+ int checkIntegerB = 0x000000FF;
+
+ ArrayBuffer buf = new ArrayBuffer(4);
+ Int32Array bufW = new Int32Array(buf);
+ Uint8Array bufR = new Uint8Array(buf);
+
+ bufW.set(0, checkIntegerA);
+
+ boolean knownBig1 = false;
+ if(bufR.get(0) == (short)0xFF && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0) {
+ knownBig1 = true;
+ }
+
+ boolean knownLittle1 = false;
+ if(bufR.get(0) == (short)0 && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0xFF) {
+ knownLittle1 = true;
+ }
+
+ bufW.set(0, checkIntegerB);
+
+ boolean knownBig2 = false;
+ if(bufR.get(0) == (short)0 && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0xFF) {
+ knownBig2 = true;
+ }
+
+ boolean knownLittle2 = false;
+ if(bufR.get(0) == (short)0xFF && bufR.get(1) == (short)0 && bufR.get(2) == (short)0 && bufR.get(3) == (short)0) {
+ knownLittle2 = true;
+ }
+
+ if(knownBig1 == knownBig2 && knownLittle1 == knownLittle2 && knownBig1 != knownLittle1) {
+ isBigEndian = knownBig1;
+ isLittleEndian = knownLittle1;
+ }
+
+ if(isBigEndian) {
+ System.out.println("This browser is BIG endian!");
+ }else if(isLittleEndian) {
+ System.out.println("This browser is LITTLE endian!");
+ }else {
+ System.out.println("The byte order of this browser is inconsistent!");
+ System.out.println(" - the sequence FF000000 was " + (knownBig1 ? "" : "not ") + "big endian.");
+ System.out.println(" - the sequence FF000000 was " + (knownLittle1 ? "" : "not ") + "little endian.");
+ System.out.println(" - the sequence 000000FF was " + (knownBig2 ? "" : "not ") + "big endian.");
+ System.out.println(" - the sequence 000000FF was " + (knownLittle2 ? "" : "not ") + "little endian.");
+ }
+
+ endianWasChecked = true;
+ }
+ return !isLittleEndian;
+ }
+
+ private static final Map relayQueryLimited = new HashMap<>();
+ private static final Map relayQueryBlocked = new HashMap<>();
+
+ private static class RelayQueryImpl implements RelayQuery {
+
+ private final WebSocket sock;
+ private final String uri;
+
+ private boolean open;
+ private boolean failed;
+
+ private boolean hasRecievedAnyData = false;
+
+ private int vers = -1;
+ private String comment = "";
+ private String brand = "";
+
+ private long connectionOpenedAt;
+ private long connectionPingStart = -1;
+ private long connectionPingTimer = -1;
+
+ private RateLimit rateLimitStatus = RateLimit.NONE;
+
+ private VersionMismatch versError = VersionMismatch.UNKNOWN;
+
+ private RelayQueryImpl(String uri) {
+ this.uri = uri;
+ WebSocket s = null;
+ try {
+ connectionOpenedAt = steadyTimeMillis();
+ s = new WebSocket(uri);
+ s.setBinaryType("arraybuffer");
+ open = true;
+ failed = false;
+ }catch(Throwable t) {
+ connectionOpenedAt = 0l;
+ sock = null;
+ open = false;
+ failed = true;
+ return;
+ }
+ sock = s;
+ sock.addEventListener("open", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ try {
+ connectionPingStart = steadyTimeMillis();
+ nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(
+ IPacket.writePacket(new IPacket00Handshake(0x03, IntegratedServer.preferredRelayVersion, ""))
+ ));
+ } catch (IOException e) {
+ System.err.println(e.toString());
+ sock.close();
+ failed = true;
+ }
+ }
+ });
+ sock.addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ if(evt.getData() != null && !isString(evt.getData())) {
+ hasRecievedAnyData = true;
+ byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
+ if(arr.length == 2 && arr[0] == (byte)0xFC) {
+ long millis = steadyTimeMillis();
+ if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ relayQueryLimited.put(RelayQueryImpl.this.uri, millis);
+ }else if(arr[1] == (byte)0x02) {
+ rateLimitStatus = RateLimit.NOW_LOCKED;
+ relayQueryLimited.put(RelayQueryImpl.this.uri, millis);
+ relayQueryBlocked.put(RelayQueryImpl.this.uri, millis);
+ }else {
+ rateLimitStatus = RateLimit.LOCKED;
+ relayQueryBlocked.put(RelayQueryImpl.this.uri, millis);
+ }
+ failed = true;
+ open = false;
+ sock.close();
+ }else {
+ if(open) {
+ try {
+ IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)));
+ if(pkt instanceof IPacket69Pong) {
+ IPacket69Pong ipkt = (IPacket69Pong)pkt;
+ versError = RelayQuery.VersionMismatch.COMPATIBLE;
+ if(connectionPingTimer == -1) {
+ connectionPingTimer = steadyTimeMillis() - connectionPingStart;
+ }
+ vers = ipkt.protcolVersion;
+ comment = ipkt.comment;
+ brand = ipkt.brand;
+ open = false;
+ failed = false;
+ sock.close();
+ }else if(pkt instanceof IPacketFFErrorCode) {
+ IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt;
+ if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) {
+ String s = ipkt.desc.toLowerCase();
+ if(s.contains("outdated client") || s.contains("client outdated")) {
+ versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED;
+ }else if(s.contains("outdated server") || s.contains("server outdated") ||
+ s.contains("outdated relay") || s.contains("server relay")) {
+ versError = RelayQuery.VersionMismatch.RELAY_OUTDATED;
+ }else {
+ versError = RelayQuery.VersionMismatch.UNKNOWN;
+ }
+ }
+ System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc);
+ open = false;
+ failed = true;
+ sock.close();
+ }else {
+ throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'");
+ }
+ } catch (IOException e) {
+ System.err.println("Relay Query Error: " + e.toString());
+ e.printStackTrace();
+ open = false;
+ failed = true;
+ sock.close();
+ }
+ }
+ }
+ }
+ }
+ });
+ sock.addEventListener("close", new EventListener() {
+ @Override
+ public void handleEvent(CloseEvent evt) {
+ open = false;
+ if(!hasRecievedAnyData) {
+ failed = true;
+ Long l = relayQueryBlocked.get(uri);
+ if(l != null) {
+ if(steadyTimeMillis() - l.longValue() < 400000l) {
+ rateLimitStatus = RateLimit.LOCKED;
+ return;
+ }
+ }
+ l = relayQueryLimited.get(uri);
+ if(l != null) {
+ if(steadyTimeMillis() - l.longValue() < 900000l) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ return;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean isQueryOpen() {
+ return open;
+ }
+
+ @Override
+ public boolean isQueryFailed() {
+ return failed;
+ }
+
+ @Override
+ public RateLimit isQueryRateLimit() {
+ return rateLimitStatus;
+ }
+
+ @Override
+ public void close() {
+ if(sock != null && open) {
+ sock.close();
+ }
+ open = false;
+ }
+
+ @Override
+ public int getVersion() {
+ return vers;
+ }
+
+ @Override
+ public String getComment() {
+ return comment;
+ }
+
+ @Override
+ public String getBrand() {
+ return brand;
+ }
+
+ @Override
+ public long getPing() {
+ return connectionPingTimer < 1 ? 1 : connectionPingTimer;
+ }
+
+ @Override
+ public VersionMismatch getCompatible() {
+ return versError;
+ }
+
+ }
+
+ private static class RelayQueryRatelimitDummy implements RelayQuery {
+
+ private final RateLimit type;
+
+ private RelayQueryRatelimitDummy(RateLimit type) {
+ this.type = type;
+ }
+
+ @Override
+ public boolean isQueryOpen() {
+ return false;
+ }
+
+ @Override
+ public boolean isQueryFailed() {
+ return true;
+ }
+
+ @Override
+ public RateLimit isQueryRateLimit() {
+ return type;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public int getVersion() {
+ return IntegratedServer.preferredRelayVersion;
+ }
+
+ @Override
+ public String getComment() {
+ return "this query was rate limited";
+ }
+
+ @Override
+ public String getBrand() {
+ return "lax1dude";
+ }
+
+ @Override
+ public long getPing() {
+ return 0l;
+ }
+
+ @Override
+ public VersionMismatch getCompatible() {
+ return VersionMismatch.COMPATIBLE;
+ }
+
+ }
+
+ public static final RelayQuery openRelayQuery(String addr) {
+ long millis = steadyTimeMillis();
+
+ Long l = relayQueryBlocked.get(addr);
+ if(l != null && millis - l.longValue() < 60000l) {
+ return new RelayQueryRatelimitDummy(RateLimit.LOCKED);
+ }
+
+ l = relayQueryLimited.get(addr);
+ if(l != null && millis - l.longValue() < 10000l) {
+ return new RelayQueryRatelimitDummy(RateLimit.BLOCKED);
+ }
+
+ return new RelayQueryImpl(addr);
+ }
+
+ private static class RelayWorldsQueryImpl implements RelayWorldsQuery {
+
+ private final WebSocket sock;
+ private final String uri;
+
+ private boolean open;
+ private boolean failed;
+
+ private boolean hasRecievedAnyData = false;
+ private RateLimit rateLimitStatus = RateLimit.NONE;
+
+ private RelayQuery.VersionMismatch versError = RelayQuery.VersionMismatch.UNKNOWN;
+
+ private List worlds = null;
+
+ private RelayWorldsQueryImpl(String uri) {
+ this.uri = uri;
+ WebSocket s = null;
+ try {
+ s = new WebSocket(uri);
+ s.setBinaryType("arraybuffer");
+ open = true;
+ failed = false;
+ }catch(Throwable t) {
+ sock = null;
+ open = false;
+ failed = true;
+ return;
+ }
+ sock = s;
+ sock.addEventListener("open", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ try {
+ nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(
+ IPacket.writePacket(new IPacket00Handshake(0x04, IntegratedServer.preferredRelayVersion, ""))
+ ));
+ } catch (IOException e) {
+ System.err.println(e.toString());
+ sock.close();
+ open = false;
+ failed = true;
+ }
+ }
+ });
+ sock.addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ if(evt.getData() != null && !isString(evt.getData())) {
+ hasRecievedAnyData = true;
+ byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
+ if(arr.length == 2 && arr[0] == (byte)0xFC) {
+ long millis = steadyTimeMillis();
+ if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis);
+ }else if(arr[1] == (byte)0x02) {
+ rateLimitStatus = RateLimit.NOW_LOCKED;
+ relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis);
+ relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis);
+ }else {
+ rateLimitStatus = RateLimit.LOCKED;
+ relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis);
+ }
+ open = false;
+ failed = true;
+ sock.close();
+ }else {
+ if(open) {
+ try {
+ IPacket pkt = IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr)));
+ if(pkt instanceof IPacket07LocalWorlds) {
+ worlds = ((IPacket07LocalWorlds)pkt).worldsList;
+ sock.close();
+ open = false;
+ failed = false;
+ }else if(pkt instanceof IPacketFFErrorCode) {
+ IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt;
+ if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) {
+ String s = ipkt.desc.toLowerCase();
+ if(s.contains("outdated client") || s.contains("client outdated")) {
+ versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED;
+ }else if(s.contains("outdated server") || s.contains("server outdated") ||
+ s.contains("outdated relay") || s.contains("server relay")) {
+ versError = RelayQuery.VersionMismatch.RELAY_OUTDATED;
+ }else {
+ versError = RelayQuery.VersionMismatch.UNKNOWN;
+ }
+ }
+ System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc);
+ open = false;
+ failed = true;
+ sock.close();
+ }else {
+ throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'");
+ }
+ } catch (IOException e) {
+ System.err.println("Relay World Query Error: " + e.toString());
+ e.printStackTrace();
+ open = false;
+ failed = true;
+ sock.close();
+ }
+ }
+ }
+ }
+ }
+ });
+ sock.addEventListener("close", new EventListener() {
+ @Override
+ public void handleEvent(CloseEvent evt) {
+ open = false;
+ if(!hasRecievedAnyData) {
+ failed = true;
+ Long l = relayQueryBlocked.get(uri);
+ if(l != null) {
+ if(steadyTimeMillis() - l.longValue() < 400000l) {
+ rateLimitStatus = RateLimit.LOCKED;
+ return;
+ }
+ }
+ l = relayQueryLimited.get(uri);
+ if(l != null) {
+ if(steadyTimeMillis() - l.longValue() < 900000l) {
+ rateLimitStatus = RateLimit.BLOCKED;
+ return;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean isQueryOpen() {
+ return open;
+ }
+
+ @Override
+ public boolean isQueryFailed() {
+ return failed;
+ }
+
+ @Override
+ public RateLimit isQueryRateLimit() {
+ return rateLimitStatus;
+ }
+
+ @Override
+ public void close() {
+ if(open && sock != null) {
+ sock.close();
+ }
+ open = false;
+ }
+
+ @Override
+ public List getWorlds() {
+ return worlds;
+ }
+
+ @Override
+ public RelayQuery.VersionMismatch getCompatible() {
+ return versError;
+ }
+
+ }
+
+ private static class RelayWorldsQueryRatelimitDummy implements RelayWorldsQuery {
+
+ private final RateLimit rateLimit;
+
+ private RelayWorldsQueryRatelimitDummy(RateLimit rateLimit) {
+ this.rateLimit = rateLimit;
+ }
+
+ @Override
+ public boolean isQueryOpen() {
+ return false;
+ }
+
+ @Override
+ public boolean isQueryFailed() {
+ return true;
+ }
+
+ @Override
+ public RateLimit isQueryRateLimit() {
+ return rateLimit;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public List getWorlds() {
+ return new ArrayList<>(0);
+ }
+
+ @Override
+ public RelayQuery.VersionMismatch getCompatible() {
+ return RelayQuery.VersionMismatch.COMPATIBLE;
+ }
+ }
+
+ public static final RelayWorldsQuery openRelayWorldsQuery(String addr) {
+ long millis = steadyTimeMillis();
+
+ Long l = relayQueryBlocked.get(addr);
+ if(l != null && millis - l.longValue() < 60000l) {
+ return new RelayWorldsQueryRatelimitDummy(RateLimit.LOCKED);
+ }
+
+ l = relayQueryLimited.get(addr);
+ if(l != null && millis - l.longValue() < 10000l) {
+ return new RelayWorldsQueryRatelimitDummy(RateLimit.BLOCKED);
+ }
+
+ return new RelayWorldsQueryImpl(addr);
+ }
+
+ private static class RelayServerSocketImpl implements RelayServerSocket {
+
+ private final WebSocket sock;
+ private final String uri;
+
+ private boolean open;
+ private boolean closed;
+ private boolean failed;
+
+ private boolean hasRecievedAnyData;
+
+ private final List exceptions = new LinkedList<>();
+ private final List packets = new LinkedList<>();
+
+ private RelayServerSocketImpl(String uri, int timeout) {
+ this.uri = uri;
+ WebSocket s = null;
+ try {
+ s = new WebSocket(uri);
+ s.setBinaryType("arraybuffer");
+ open = false;
+ closed = false;
+ failed = false;
+ }catch(Throwable t) {
+ exceptions.add(t);
+ sock = null;
+ open = false;
+ closed = true;
+ failed = true;
+ return;
+ }
+ sock = s;
+ sock.addEventListener("open", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ open = true;
+ }
+ });
+ sock.addEventListener("message", new EventListener() {
+ @Override
+ public void handleEvent(MessageEvent evt) {
+ if(evt.getData() != null && !isString(evt.getData())) {
+ hasRecievedAnyData = true;
+ byte[] arr = TeaVMUtils.wrapByteArrayBuffer(evt.getDataAsArray());
+ try {
+ packets.add(IPacket.readPacket(new DataInputStream(new EaglerInputStream(arr))));
+ } catch (IOException e) {
+ exceptions.add(e);
+ System.err.println("Relay Socket Error: " + e.toString());
+ e.printStackTrace();
+ open = false;
+ failed = true;
+ closed = true;
+ sock.close();
+ }
+ }
+ }
+ });
+ sock.addEventListener("close", new EventListener() {
+ @Override
+ public void handleEvent(CloseEvent evt) {
+ if(!hasRecievedAnyData) {
+ failed = true;
+ }
+ open = false;
+ closed = true;
+ }
+ });
+ Window.setTimeout(new TimerHandler() {
+
+ @Override
+ public void onTimer() {
+ if(!open && !closed) {
+ closed = true;
+ sock.close();
+ }
+ }
+
+ }, timeout);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public void close() {
+ if(open && sock != null) {
+ sock.close();
+ }
+ open = false;
+ closed = true;
+ }
+
+ @Override
+ public boolean isFailed() {
+ return failed;
+ }
+
+ @Override
+ public Throwable getException() {
+ if(exceptions.size() > 0) {
+ return exceptions.remove(0);
+ }else {
+ return null;
+ }
+ }
+
+ @Override
+ public void writePacket(IPacket pkt) {
+ try {
+ nativeBinarySend(sock, TeaVMUtils.unwrapArrayBuffer(IPacket.writePacket(pkt)));
+ } catch (Throwable e) {
+ System.err.println("Relay connection error: " + e.toString());
+ e.printStackTrace();
+ exceptions.add(e);
+ failed = true;
+ open = false;
+ closed = true;
+ sock.close();
+ }
+ }
+
+ @Override
+ public IPacket readPacket() {
+ if(packets.size() > 0) {
+ return packets.remove(0);
+ }else {
+ return null;
+ }
+ }
+
+ @Override
+ public IPacket nextPacket() {
+ if(packets.size() > 0) {
+ return packets.get(0);
+ }else {
+ return null;
+ }
+ }
+
+ @Override
+ public RateLimit getRatelimitHistory() {
+ if(relayQueryBlocked.containsKey(uri)) {
+ return RateLimit.LOCKED;
+ }
+ if(relayQueryLimited.containsKey(uri)) {
+ return RateLimit.BLOCKED;
+ }
+ return RateLimit.NONE;
+ }
+
+ @Override
+ public String getURI() {
+ return uri;
+ }
+
+ }
+
+ private static class RelayServerSocketRatelimitDummy implements RelayServerSocket {
+
+ private final RateLimit limit;
+
+ private RelayServerSocketRatelimitDummy(RateLimit limit) {
+ this.limit = limit;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return false;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return true;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public boolean isFailed() {
+ return true;
+ }
+
+ @Override
+ public Throwable getException() {
+ return null;
+ }
+
+ @Override
+ public void writePacket(IPacket pkt) {
+ }
+
+ @Override
+ public IPacket readPacket() {
+ return null;
+ }
+
+ @Override
+ public IPacket nextPacket() {
+ return null;
+ }
+
+ @Override
+ public RateLimit getRatelimitHistory() {
+ return limit;
+ }
+
+ @Override
+ public String getURI() {
+ return "";
+ }
+
+ }
+
+ public static final RelayServerSocket openRelayConnection(String addr, int timeout) {
+ long millis = steadyTimeMillis();
+
+ Long l = relayQueryBlocked.get(addr);
+ if(l != null && millis - l.longValue() < 60000l) {
+ return new RelayServerSocketRatelimitDummy(RateLimit.LOCKED);
+ }
+
+ l = relayQueryLimited.get(addr);
+ if(l != null && millis - l.longValue() < 10000l) {
+ return new RelayServerSocketRatelimitDummy(RateLimit.BLOCKED);
+ }
+
+ return new RelayServerSocketImpl(addr, timeout);
+ }
+
+ private static EaglercraftLANClient rtcLANClient = null;
+
+ @JSBody(params = { }, script = "return window.startLANClient();")
+ private static native EaglercraftLANClient startRTCLANClient();
+
+ private static boolean clientLANinit = false;
+ private static final List clientLANPacketBuffer = new ArrayList<>();
+
+ private static String clientICECandidate = null;
+ private static String clientDescription = null;
+ private static boolean clientDataChannelOpen = false;
+ private static boolean clientDataChannelClosed = true;
+
+ public static final boolean clientLANSupported() {
+ return rtcLANClient.LANClientSupported();
+ }
+
+ public static final int clientLANReadyState() {
+ return rtcLANClient.getReadyState();
+ }
+
+ public static final void clientLANCloseConnection() {
+ rtcLANClient.signalRemoteDisconnect(false);
+ }
+
+ public static final void clientLANSendPacket(byte[] pkt) {
+ rtcLANClient.sendPacketToServer(TeaVMUtils.unwrapArrayBuffer(pkt));
+ }
+
+ public static final byte[] clientLANReadPacket() {
+ return clientLANPacketBuffer.size() > 0 ? clientLANPacketBuffer.remove(0) : null;
+ }
+
+ public static final void clientLANSetICEServersAndConnect(String[] servers) {
+ if(!clientLANinit) {
+ clientLANinit = true;
+ rtcLANClient.setDescriptionHandler(new EaglercraftLANClient.DescriptionHandler() {
+ @Override
+ public void call(String description) {
+ clientDescription = description;
+ }
+ });
+ rtcLANClient.setICECandidateHandler(new EaglercraftLANClient.ICECandidateHandler() {
+ @Override
+ public void call(String candidate) {
+ clientICECandidate = candidate;
+ }
+ });
+ rtcLANClient.setRemoteDataChannelHandler(new EaglercraftLANClient.ClientSignalHandler() {
+ @Override
+ public void call() {
+ clientDataChannelClosed = false;
+ clientDataChannelOpen = true;
+ }
+ });
+ rtcLANClient.setRemotePacketHandler(new EaglercraftLANClient.RemotePacketHandler() {
+ @Override
+ public void call(ArrayBuffer buffer) {
+ clientLANPacketBuffer.add(TeaVMUtils.wrapByteArrayBuffer(buffer));
+ }
+ });
+ rtcLANClient.setRemoteDisconnectHandler(new EaglercraftLANClient.ClientSignalHandler() {
+ @Override
+ public void call() {
+ clientDataChannelClosed = true;
+ }
+ });
+ }
+ rtcLANClient.setICEServers(servers);
+ if(clientLANReadyState() == rtcLANClient.READYSTATE_CONNECTED || clientLANReadyState() == rtcLANClient.READYSTATE_CONNECTING) {
+ rtcLANClient.signalRemoteDisconnect(true);
+ }
+ rtcLANClient.initializeClient();
+ rtcLANClient.signalRemoteConnect();
+ }
+
+ public static final void clearLANClientState() {
+ clientICECandidate = null;
+ clientDescription = null;
+ clientDataChannelOpen = false;
+ clientDataChannelClosed = true;
+ }
+
+ public static final String clientLANAwaitICECandidate() {
+ if(clientICECandidate != null) {
+ String ret = clientICECandidate;
+ clientICECandidate = null;
+ return ret;
+ }else {
+ return null;
+ }
+ }
+
+ public static final String clientLANAwaitDescription() {
+ if(clientDescription != null) {
+ String ret = clientDescription;
+ clientDescription = null;
+ return ret;
+ }else {
+ return null;
+ }
+ }
+
+ public static final boolean clientLANAwaitChannel() {
+ if(clientDataChannelOpen) {
+ clientDataChannelOpen = false;
+ return true;
+ }else {
+ return false;
+ }
+ }
+
+ public static final boolean clientLANClosed() {
+ return clientDataChannelClosed;
+ }
+
+ public static final void clientLANSetICECandidate(String candidate) {
+ rtcLANClient.signalRemoteICECandidate(candidate);
+ }
+
+ public static final void clientLANSetDescription(String description) {
+ rtcLANClient.signalRemoteDescription(description);
+ }
+
+ private static EaglercraftLANServer rtcLANServer = null;
+
+ @JSBody(params = { }, script = "return window.startLANServer();")
+ private static native EaglercraftLANServer startRTCLANServer();
+
+ private static boolean serverLANinit = false;
+ private static final List serverLANEventBuffer = new LinkedList<>();
+
+ public static final boolean serverLANSupported() {
+ return rtcLANServer.LANServerSupported();
+ }
+
+ public static final void serverLANInitializeServer(String[] servers) {
+ serverLANEventBuffer.clear();
+ rtcLANServer.setICEServers(servers);
+ rtcLANServer.initializeServer();
+ if(!serverLANinit) {
+ serverLANinit = true;
+ rtcLANServer.setDescriptionHandler(new EaglercraftLANServer.DescriptionHandler() {
+ @Override
+ public void call(String peerId, String description) {
+ serverLANEventBuffer.add(new LANPeerEvent.LANPeerDescriptionEvent(peerId, description));
+ }
+ });
+ rtcLANServer.setICECandidateHandler(new EaglercraftLANServer.ICECandidateHandler() {
+ @Override
+ public void call(String peerId, String candidate) {
+ serverLANEventBuffer.add(new LANPeerEvent.LANPeerICECandidateEvent(peerId, candidate));
+ }
+ });
+ rtcLANServer.setRemoteClientDataChannelHandler(new EaglercraftLANServer.ClientSignalHandler() {
+ @Override
+ public void call(String peerId) {
+ serverLANEventBuffer.add(new LANPeerEvent.LANPeerDataChannelEvent(peerId));
+ }
+ });
+ rtcLANServer.setRemoteClientPacketHandler(new EaglercraftLANServer.PeerPacketHandler() {
+ @Override
+ public void call(String peerId, ArrayBuffer buffer) {
+ serverLANEventBuffer.add(new LANPeerEvent.LANPeerPacketEvent(peerId, TeaVMUtils.wrapByteArrayBuffer(buffer)));
+ }
+ });
+ rtcLANServer.setRemoteClientDisconnectHandler(new EaglercraftLANServer.ClientSignalHandler() {
+ @Override
+ public void call(String peerId) {
+ serverLANEventBuffer.add(new LANPeerEvent.LANPeerDisconnectEvent(peerId));
+ }
+ });
+ }
+ }
+
+ public static final void serverLANCloseServer() {
+ rtcLANServer.signalRemoteDisconnect("");
+ }
+
+ public static final LANPeerEvent serverLANGetEvent(String clientId) {
+ if(serverLANEventBuffer.size() > 0) {
+ Iterator i = serverLANEventBuffer.iterator();
+ while(i.hasNext()) {
+ LANPeerEvent evt = i.next();
+ if(evt.getPeerId().equals(clientId)) {
+ i.remove();
+ return evt;
+ }
+ }
+ return null;
+ }else {
+ return null;
+ }
+ }
+
+ public static final List serverLANGetAllEvent(String clientId) {
+ if(serverLANEventBuffer.size() > 0) {
+ List lst = null;
+ Iterator i = serverLANEventBuffer.iterator();
+ while(i.hasNext()) {
+ LANPeerEvent evt = i.next();
+ if(evt.getPeerId().equals(clientId)) {
+ i.remove();
+ if(lst == null) {
+ lst = new ArrayList<>();
+ }
+ lst.add(evt);
+ }
+ }
+ return lst;
+ }else {
+ return null;
+ }
+ }
+
+ private static final int fragmentSize = 0xFF00;
+
+ public static final void serverLANWritePacket(String peer, byte[] data) {
+ if (data.length > fragmentSize) {
+ for (int i = 0; i < data.length; i += fragmentSize) {
+ byte[] fragData = new byte[((i + fragmentSize > data.length) ? (data.length % fragmentSize) : fragmentSize) + 1];
+ System.arraycopy(data, i, fragData, 1, fragData.length - 1);
+ fragData[0] = (i + fragmentSize < data.length) ? (byte) 1 : (byte) 0;
+ rtcLANServer.sendPacketToRemoteClient(peer, TeaVMUtils.unwrapArrayBuffer(fragData));
+ }
+ } else {
+ byte[] sendData = new byte[data.length + 1];
+ sendData[0] = 0;
+ System.arraycopy(data, 0, sendData, 1, data.length);
+ rtcLANServer.sendPacketToRemoteClient(peer, TeaVMUtils.unwrapArrayBuffer(sendData));
+ }
+ }
+
+ public static final void serverLANCreatePeer(String peer) {
+ rtcLANServer.signalRemoteConnect(peer);
+ }
+
+ public static final void serverLANPeerICECandidates(String peer, String iceCandidates) {
+ rtcLANServer.signalRemoteICECandidate(peer, iceCandidates);
+ }
+
+ public static final void serverLANPeerDescription(String peer, String description) {
+ rtcLANServer.signalRemoteDescription(peer, description);
+ }
+
+ public static final void serverLANDisconnectPeer(String peer) {
+ rtcLANServer.signalRemoteDisconnect(peer);
+ }
+
+ public static final int countPeers() {
+ return rtcLANServer.countPeers();
+ }
+
+ private static final JSObject steadyTimeFunc = getSteadyTimeFunc();
+
+ @JSBody(params = { }, script = "return ((typeof performance !== \"undefined\") && (typeof performance.now === \"function\"))"
+ + "? performance.now.bind(performance)"
+ + ": (function(epochStart){ return function() { return Date.now() - epochStart; }; })(Date.now());")
+ private static native JSObject getSteadyTimeFunc();
+
+ @JSBody(params = { "steadyTimeFunc" }, script = "return steadyTimeFunc();")
+ private static native double steadyTimeMillis0(JSObject steadyTimeFunc);
+
+ public static long steadyTimeMillis() {
+ return (long)steadyTimeMillis0(steadyTimeFunc);
+ }
+
+ public static long nanoTime() {
+ return (long)(steadyTimeMillis0(steadyTimeFunc) * 1000000.0);
+ }
+
+ @Async
+ public static native void sleep(int millis);
+
+ private static void sleep(int millis, final AsyncCallback callback) {
+ Platform.schedule(new DumbSleepHandler(callback), millis);
+ }
+
+ private static class DumbSleepHandler implements PlatformRunnable {
+ private final AsyncCallback callback;
+ private DumbSleepHandler(AsyncCallback callback) {
+ this.callback = callback;
+ }
+ @Override
+ public void run() {
+ callback.complete(null);
+ }
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/SimpleStorage.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/SimpleStorage.java
new file mode 100644
index 0000000..3974610
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/SimpleStorage.java
@@ -0,0 +1,190 @@
+package net.lax1dude.eaglercraft.adapter;
+
+import org.teavm.interop.Async;
+import org.teavm.interop.AsyncCallback;
+import org.teavm.jso.core.JSString;
+import org.teavm.jso.indexeddb.IDBDatabase;
+import org.teavm.jso.indexeddb.IDBFactory;
+import org.teavm.jso.indexeddb.IDBGetRequest;
+import org.teavm.jso.indexeddb.IDBObjectStore;
+import org.teavm.jso.indexeddb.IDBOpenDBRequest;
+import org.teavm.jso.indexeddb.IDBRequest;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+
+import net.lax1dude.eaglercraft.adapter.teavm.TeaVMUtils;
+
+public class SimpleStorage {
+ private static IDBDatabase database;
+ private static boolean available;
+
+ public static boolean isAvailable() {
+ return available;
+ }
+
+ static {
+ IDBOpenDBRequest request = IDBFactory.getInstance().open("eagstorage", 1);
+ request.setOnUpgradeNeeded(evt -> {
+ database = request.getResult();
+ database.createObjectStore("store");
+ });
+ request.setOnSuccess(() -> {
+ database = request.getResult();
+ available = true;
+ });
+ request.setOnError(() -> {
+ database = request.getResult();
+ available = false;
+ });
+ }
+
+ private static IDBObjectStore getStore() {
+ return database.transaction(new String[] { "store" }, "readwrite").objectStore("store");
+ }
+
+ @Async
+ public static native byte[] get(String key);
+
+ private static void get(String key, final AsyncCallback cb) {
+ if (key.equals("__LIST__") || key.contains("\n")) {
+ cb.complete(null);
+ return;
+ }
+ IDBGetRequest request = getStore().get(JSString.valueOf(key));
+ request.setOnSuccess(() -> {
+ cb.complete(TeaVMUtils.wrapByteArrayBuffer((ArrayBuffer) request.getResult()));
+ });
+ request.setOnError(() -> {
+ cb.complete(null);
+ });
+ }
+
+ @Async
+ public static native Boolean set(String key, byte[] value);
+
+ private static void set(String key, byte[] value, final AsyncCallback cb) {
+ if (key.equals("__LIST__") || key.contains("\n")) {
+ cb.complete(false);
+ return;
+ }
+ if (value == null) {
+ IDBGetRequest request3 = getStore().get(JSString.valueOf("__LIST__"));
+ request3.setOnSuccess(() -> {
+ String listVal;
+ if (JSString.isInstance(request3.getResult()) && !(listVal = ((JSString) request3.getResult().cast()).stringValue()).isEmpty()) {
+ String[] list = listVal.replaceAll("[^a-zA-Z0-9_\n]", "").split("\n");
+ String[] newList = new String[list.length - 1];
+ int a = 0;
+ for (int i = 0; i < list.length; ++i) {
+ if (list[i].equals(key)) {
+ --a;
+ } else {
+ newList[i + a] = list[i];
+ }
+ }
+ IDBRequest request2 = getStore().put(JSString.valueOf(String.join("\n", newList)), JSString.valueOf("__LIST__"));
+ request2.setOnSuccess(() -> {
+ IDBRequest request = getStore().delete(JSString.valueOf(key));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ });
+ request2.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ } else {
+ IDBRequest request = getStore().delete(JSString.valueOf(key));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ }
+ });
+ request3.setOnError(() -> {
+ IDBRequest request = getStore().delete(JSString.valueOf(key));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ });
+ } else {
+ ArrayBuffer arr = TeaVMUtils.unwrapArrayBuffer(value);
+ IDBRequest request2 = getStore().put(arr, JSString.valueOf(key));
+ request2.setOnSuccess(() -> {
+ IDBGetRequest request3 = getStore().get(JSString.valueOf("__LIST__"));
+ request3.setOnSuccess(() -> {
+ String listVal;
+ if (JSString.isInstance(request3.getResult()) && !(listVal = ((JSString) request3.getResult().cast()).stringValue()).isEmpty()) {
+ String[] list = listVal.replaceAll("[^a-zA-Z0-9_\n]", "").split("\n");
+ boolean alrHas = false;
+ for (String s : list) {
+ if (s.equals(key)) {
+ alrHas = true;
+ break;
+ }
+ }
+ String[] newList;
+ if (alrHas) {
+ newList = list;
+ } else {
+ newList = new String[list.length + 1];
+ System.arraycopy(list, 0, newList, 0, list.length);
+ newList[list.length] = key;
+ }
+ IDBRequest request = getStore().put(JSString.valueOf(String.join("\n", newList).replaceAll("[^a-zA-Z0-9_\n]", "")), JSString.valueOf("__LIST__"));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ } else {
+ IDBRequest request = getStore().put(JSString.valueOf(key), JSString.valueOf("__LIST__"));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ }
+ });
+ request3.setOnError(() -> {
+ IDBRequest request = getStore().put(JSString.valueOf(key), JSString.valueOf("__LIST__"));
+ request.setOnSuccess(() -> {
+ cb.complete(Boolean.TRUE);
+ });
+ request.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ });
+ });
+ request2.setOnError(() -> {
+ cb.complete(Boolean.FALSE);
+ });
+ }
+ }
+
+ @Async
+ public static native String[] list();
+
+ private static void list(final AsyncCallback cb) {
+ IDBGetRequest request = getStore().get(JSString.valueOf("__LIST__"));
+ request.setOnSuccess(() -> {
+ String listVal;
+ if (JSString.isInstance(request.getResult()) && !(listVal = ((JSString) request.getResult().cast()).stringValue()).isEmpty()) {
+ cb.complete(listVal.replaceAll("[^a-zA-Z0-9_\n]", "").split("\n"));
+ } else {
+ cb.complete(new String[0]);
+ }
+ });
+ request.setOnError(() -> {
+ cb.complete(new String[0]);
+ });
+ }
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/Tessellator.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/Tessellator.java
new file mode 100644
index 0000000..88ffd0a
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/Tessellator.java
@@ -0,0 +1,386 @@
+package net.lax1dude.eaglercraft.adapter;
+
+import org.teavm.jso.typedarrays.ArrayBuffer;
+import org.teavm.jso.typedarrays.Float32Array;
+import org.teavm.jso.typedarrays.Int32Array;
+
+import net.lax1dude.eaglercraft.EaglerAdapter;
+
+
+public class Tessellator {
+
+ /** The byte buffer used for GL allocation. */
+ private Int32Array intBuffer;
+ private Float32Array floatBuffer;
+
+ /**
+ * The number of vertices to be drawn in the next draw call. Reset to 0 between
+ * draw calls.
+ */
+ private int vertexCount = 0;
+
+ /** The first coordinate to be used for the texture. */
+ private float textureU;
+
+ /** The second coordinate to be used for the texture. */
+ private float textureV;
+ private int brightness;
+
+ /** The color (RGBA) value to be used for the following draw call. */
+ private int color;
+
+ /**
+ * Whether the current draw object for this tessellator has color values.
+ */
+ private boolean hasColor = false;
+
+ /**
+ * Whether the current draw object for this tessellator has texture coordinates.
+ */
+ private boolean hasTexture = false;
+ private boolean hasBrightness = false;
+
+ /**
+ * Whether the current draw object for this tessellator has normal values.
+ */
+ private boolean hasNormals = false;
+
+ /** The index into the raw buffer to be used for the next data. */
+ private int rawBufferIndex = 0;
+
+ /**
+ * The number of vertices manually added to the given draw call. This differs
+ * from vertexCount because it adds extra vertices when converting quads to
+ * triangles.
+ */
+ private int addedVertices = 0;
+
+ /** Disables all color information for the following draw call. */
+ private boolean isColorDisabled = false;
+
+ /** The draw mode currently being used by the tessellator. */
+ private int drawMode;
+
+ /**
+ * An offset to be applied along the x-axis for all vertices in this draw call.
+ */
+ private double xOffset;
+
+ /**
+ * An offset to be applied along the y-axis for all vertices in this draw call.
+ */
+ private double yOffset;
+
+ /**
+ * An offset to be applied along the z-axis for all vertices in this draw call.
+ */
+ private double zOffset;
+
+ /** The normal to be applied to the face being drawn. */
+ private int normal;
+
+ /** The static instance of the Tessellator. */
+ public static final Tessellator instance = new Tessellator(525000);
+
+ /** Whether this tessellator is currently in draw mode. */
+ private boolean isDrawing = false;
+
+ /** Whether we are currently using VBO or not. */
+ private boolean useVBO = false;
+
+ /** The size of the buffers used (in integers). */
+ private int bufferSize;
+
+ private Tessellator(int par1) {
+ this.bufferSize = par1;
+ ArrayBuffer a = new ArrayBuffer(par1 * 4);
+ this.intBuffer = new Int32Array(a);
+ this.floatBuffer = new Float32Array(a);
+ }
+
+ /**
+ * Draws the data set up in this tessellator and resets the state to prepare for
+ * new drawing.
+ */
+ public int draw() {
+ if (!this.isDrawing) {
+ return 0;
+ } else {
+ this.isDrawing = false;
+
+ if (this.vertexCount > 0) {
+
+ if (this.hasTexture) {
+ EaglerAdapter.glEnableVertexAttrib(EaglerAdapter.GL_TEXTURE_COORD_ARRAY);
+ }
+
+ if (this.hasColor) {
+ EaglerAdapter.glEnableVertexAttrib(EaglerAdapter.GL_COLOR_ARRAY);
+ }
+
+ if (this.hasNormals) {
+ EaglerAdapter.glEnableVertexAttrib(EaglerAdapter.GL_NORMAL_ARRAY);
+ }
+
+ if (this.hasBrightness) {
+ EaglerAdapter.glClientActiveTexture(EaglerAdapter.GL_TEXTURE1);
+ EaglerAdapter.glEnableVertexAttrib(EaglerAdapter.GL_TEXTURE_COORD_ARRAY);
+ EaglerAdapter.glClientActiveTexture(EaglerAdapter.GL_TEXTURE0);
+ }
+
+ EaglerAdapter.glDrawArrays(this.drawMode, 0, this.vertexCount, new Int32Array(intBuffer.getBuffer(), 0, this.vertexCount * 8));
+
+ if (this.hasTexture) {
+ EaglerAdapter.glDisableVertexAttrib(EaglerAdapter.GL_TEXTURE_COORD_ARRAY);
+ }
+
+ if (this.hasColor) {
+ EaglerAdapter.glDisableVertexAttrib(EaglerAdapter.GL_COLOR_ARRAY);
+ }
+
+ if (this.hasNormals) {
+ EaglerAdapter.glDisableVertexAttrib(EaglerAdapter.GL_NORMAL_ARRAY);
+ }
+
+ if (this.hasBrightness) {
+ EaglerAdapter.glClientActiveTexture(EaglerAdapter.GL_TEXTURE1);
+ EaglerAdapter.glDisableVertexAttrib(EaglerAdapter.GL_TEXTURE_COORD_ARRAY);
+ EaglerAdapter.glClientActiveTexture(EaglerAdapter.GL_TEXTURE0);
+ }
+ }
+
+ int var1 = this.rawBufferIndex * 4;
+ this.reset();
+ return var1;
+ }
+ }
+
+ /**
+ * Clears the tessellator state in preparation for new drawing.
+ */
+ private void reset() {
+ this.vertexCount = 0;
+ //this.byteBuffer.clear();
+ this.rawBufferIndex = 0;
+ this.addedVertices = 0;
+ }
+
+ /**
+ * Sets draw mode in the tessellator to draw quads.
+ */
+ public void startDrawingQuads() {
+ this.startDrawing(EaglerAdapter.GL_QUADS);
+ }
+
+ /**
+ * Resets tessellator state and prepares for drawing (with the specified draw
+ * mode).
+ */
+ public void startDrawing(int par1) {
+ if (this.isDrawing) {
+ this.draw();
+ }
+ this.isDrawing = true;
+ this.reset();
+ this.drawMode = par1;
+ this.hasNormals = false;
+ this.hasColor = false;
+ this.hasTexture = false;
+ this.hasBrightness = false;
+ this.isColorDisabled = false;
+ }
+
+ /**
+ * Sets the texture coordinates.
+ */
+ public void setTextureUV(double par1, double par3) {
+ this.hasTexture = true;
+ this.textureU = (float) par1;
+ this.textureV = (float) par3;
+ }
+
+ public void setBrightness(int par1) {
+ this.hasBrightness = true;
+ this.brightness = par1;
+ }
+
+ /**
+ * Sets the RGB values as specified, converting from floats between 0 and 1 to
+ * integers from 0-255.
+ */
+ public void setColorOpaque_F(float par1, float par2, float par3) {
+ this.setColorOpaque((int) (par1 * 255.0F), (int) (par2 * 255.0F), (int) (par3 * 255.0F));
+ }
+
+ /**
+ * Sets the RGBA values for the color, converting from floats between 0 and 1 to
+ * integers from 0-255.
+ */
+ public void setColorRGBA_F(float par1, float par2, float par3, float par4) {
+ this.setColorRGBA((int) (par1 * 255.0F), (int) (par2 * 255.0F), (int) (par3 * 255.0F), (int) (par4 * 255.0F));
+ }
+
+ /**
+ * Sets the RGB values as specified, and sets alpha to opaque.
+ */
+ public void setColorOpaque(int par1, int par2, int par3) {
+ this.setColorRGBA(par1, par2, par3, 255);
+ }
+
+ /**
+ * Sets the RGBA values for the color. Also clamps them to 0-255.
+ */
+ public void setColorRGBA(int par1, int par2, int par3, int par4) {
+ if (!this.isColorDisabled) {
+ if (par1 > 255) {
+ par1 = 255;
+ }
+
+ if (par2 > 255) {
+ par2 = 255;
+ }
+
+ if (par3 > 255) {
+ par3 = 255;
+ }
+
+ if (par4 > 255) {
+ par4 = 255;
+ }
+
+ if (par1 < 0) {
+ par1 = 0;
+ }
+
+ if (par2 < 0) {
+ par2 = 0;
+ }
+
+ if (par3 < 0) {
+ par3 = 0;
+ }
+
+ if (par4 < 0) {
+ par4 = 0;
+ }
+
+ this.hasColor = true;
+ this.color = par4 << 24 | par3 << 16 | par2 << 8 | par1;
+ }
+ }
+
+ /**
+ * Adds a vertex specifying both x,y,z and the texture u,v for it.
+ */
+ public void addVertexWithUV(double par1, double par3, double par5, double par7, double par9) {
+ this.setTextureUV(par7, par9);
+ this.addVertex(par1, par3, par5);
+ }
+
+ /**
+ * Adds a vertex with the specified x,y,z to the current draw call. It will
+ * trigger a draw() if the buffer gets full.
+ */
+ public void addVertex(double par1, double par3, double par5) {
+ if(this.addedVertices > 65534) return;
+ ++this.addedVertices;
+ ++this.vertexCount;
+
+ int bufferIndex = this.rawBufferIndex;
+ Int32Array intBuffer0 = intBuffer;
+ Float32Array floatBuffer0 = floatBuffer;
+
+ floatBuffer0.set(bufferIndex + 0, (float) (par1 + this.xOffset));
+ floatBuffer0.set(bufferIndex + 1, (float) (par3 + this.yOffset));
+ floatBuffer0.set(bufferIndex + 2, (float) (par5 + this.zOffset));
+
+ if (this.hasTexture) {
+ floatBuffer0.set(bufferIndex + 3, this.textureU);
+ floatBuffer0.set(bufferIndex + 4, this.textureV);
+ }
+
+ if (this.hasColor) {
+ intBuffer0.set(bufferIndex + 5, this.color);
+ }
+
+ if (this.hasNormals) {
+ intBuffer0.set(bufferIndex + 6, this.normal);
+ }
+
+ if (this.hasBrightness) {
+ intBuffer0.set(bufferIndex + 7, this.brightness);
+ }
+
+ this.rawBufferIndex += 8;
+ }
+
+ /**
+ * Sets the color to the given opaque value (stored as byte values packed in an
+ * integer).
+ */
+ public void setColorOpaque_I(int par1) {
+ int var2 = par1 >>> 16 & 255;
+ int var3 = par1 >>> 8 & 255;
+ int var4 = par1 & 255;
+ this.setColorOpaque(var2, var3, var4);
+ }
+
+ /**
+ * Sets the color to the given color (packed as bytes in integer) and alpha
+ * values.
+ */
+ public void setColorRGBA_I(int par1, int par2) {
+ int var3 = par1 >>> 16 & 255;
+ int var4 = par1 >>> 8 & 255;
+ int var5 = par1 & 255;
+ this.setColorRGBA(var3, var4, var5, par2);
+ }
+
+ /**
+ * Disables colors for the current draw call.
+ */
+ public void disableColor() {
+ this.isColorDisabled = true;
+ }
+
+ /**
+ * Sets the normal for the current draw call.
+ */
+ public void setNormal(float par1, float par2, float par3) {
+ this.hasNormals = true;
+ int var4 = (int)(par1 * 127.0F) + 127;
+ int var5 = (int)(par2 * 127.0F) + 127;
+ int var6 = (int)(par3 * 127.0F) + 127;
+ this.normal = var4 & 255 | (var5 & 255) << 8 | (var6 & 255) << 16;
+ }
+
+ /**
+ * Sets the normal for the current draw call.
+ */
+ public void setNormalN(float par1, float par2, float par3) {
+ this.hasNormals = true;
+ float len = (float) Math.sqrt(par1 * par1 + par2 * par2 + par3 * par3);
+ int var4 = (int)((par1 / len) * 127.0F) + 127;
+ int var5 = (int)((par2 / len) * 127.0F) + 127;
+ int var6 = (int)((par3 / len) * 127.0F) + 127;
+ this.normal = var4 & 255 | (var5 & 255) << 8 | (var6 & 255) << 16;
+ }
+
+ /**
+ * Sets the translation for all vertices in the current draw call.
+ */
+ public void setTranslation(double par1, double par3, double par5) {
+ this.xOffset = par1;
+ this.yOffset = par3;
+ this.zOffset = par5;
+ }
+
+ /**
+ * Offsets the translation for all vertices in the current draw call.
+ */
+ public void addTranslation(float par1, float par2, float par3) {
+ this.xOffset += (float) par1;
+ this.yOffset += (float) par2;
+ this.zOffset += (float) par3;
+ }
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/BufferConverter.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/BufferConverter.java
new file mode 100644
index 0000000..879d594
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/BufferConverter.java
@@ -0,0 +1,59 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import org.teavm.jso.typedarrays.Int8Array;
+import org.teavm.jso.typedarrays.Uint8Array;
+
+public class BufferConverter {
+
+ public static final Int8Array convertByteBuffer(ByteBuffer b) {
+ if(b.hasArray()) {
+ int p = b.position();
+ int l = b.remaining();
+ return new Int8Array(TeaVMUtils.unwrapArrayBuffer(b.array()), p, l);
+ }else {
+ byte[] ret = new byte[b.remaining()];
+ b.get(ret);
+ return TeaVMUtils.unwrapByteArray(ret);
+ }
+ }
+
+ public static final Uint8Array convertByteBufferUnsigned(ByteBuffer b) {
+ if(b.hasArray()) {
+ int p = b.position();
+ int l = b.remaining();
+ return new Uint8Array(TeaVMUtils.unwrapArrayBuffer(b.array()), p, l);
+ }else {
+ byte[] ret = new byte[b.remaining()];
+ b.get(ret);
+ return TeaVMUtils.unwrapUnsignedByteArray(ret);
+ }
+ }
+
+ public static final Int8Array convertIntBuffer(IntBuffer b) {
+ if(b.hasArray()) {
+ int p = b.position() << 2;
+ int l = b.remaining() << 2;
+ return new Int8Array(TeaVMUtils.unwrapArrayBuffer(b.array()), p, l);
+ }else {
+ int[] ret = new int[b.remaining()];
+ b.get(ret);
+ return new Int8Array(TeaVMUtils.unwrapArrayBuffer(ret));
+ }
+ }
+
+ public static final Uint8Array convertIntBufferUnsigned(IntBuffer b) {
+ if(b.hasArray()) {
+ int p = b.position() << 2;
+ int l = b.remaining() << 2;
+ return new Uint8Array(TeaVMUtils.unwrapArrayBuffer(b.array()), p, l);
+ }else {
+ int[] ret = new int[b.remaining()];
+ b.get(ret);
+ return new Uint8Array(TeaVMUtils.unwrapArrayBuffer(ret));
+ }
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java
new file mode 100644
index 0000000..0b2d0e8
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANClient.java
@@ -0,0 +1,63 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+
+public interface EaglercraftLANClient extends JSObject {
+
+ final int READYSTATE_INIT_FAILED = -2;
+ final int READYSTATE_FAILED = -1;
+ final int READYSTATE_DISCONNECTED = 0;
+ final int READYSTATE_CONNECTING = 1;
+ final int READYSTATE_CONNECTED = 2;
+
+ boolean LANClientSupported();
+
+ void initializeClient();
+
+ void setICEServers(String[] urls);
+
+ void setICECandidateHandler(ICECandidateHandler callback);
+
+ void setDescriptionHandler(DescriptionHandler callback);
+
+ void setRemoteDataChannelHandler(ClientSignalHandler cb);
+
+ void setRemoteDisconnectHandler(ClientSignalHandler cb);
+
+ void setRemotePacketHandler(RemotePacketHandler cb);
+
+ int getReadyState();
+
+ void sendPacketToServer(ArrayBuffer buffer);
+
+ void signalRemoteConnect();
+
+ void signalRemoteDescription(String descJSON);
+
+ void signalRemoteICECandidate(String candidate);
+
+ void signalRemoteDisconnect(boolean quiet);
+
+ @JSFunctor
+ public static interface ICECandidateHandler extends JSObject {
+ void call(String candidate);
+ }
+
+ @JSFunctor
+ public static interface DescriptionHandler extends JSObject {
+ void call(String description);
+ }
+
+ @JSFunctor
+ public static interface ClientSignalHandler extends JSObject {
+ void call();
+ }
+
+ @JSFunctor
+ public static interface RemotePacketHandler extends JSObject {
+ void call(ArrayBuffer buffer);
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java
new file mode 100644
index 0000000..06ca4fe
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftLANServer.java
@@ -0,0 +1,57 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+
+public interface EaglercraftLANServer extends JSObject {
+
+ boolean LANServerSupported();
+
+ void initializeServer();
+
+ void setICEServers(String[] urls);
+
+ void setICECandidateHandler(ICECandidateHandler cb);
+
+ void setDescriptionHandler(DescriptionHandler cb);
+
+ void setRemoteClientDataChannelHandler(ClientSignalHandler cb);
+
+ void setRemoteClientDisconnectHandler(ClientSignalHandler cb);
+
+ void setRemoteClientPacketHandler(PeerPacketHandler cb);
+
+ void sendPacketToRemoteClient(String peerId, ArrayBuffer buffer);
+
+ void signalRemoteConnect(String peerId);
+
+ void signalRemoteDescription(String peerId, String descJSON);
+
+ void signalRemoteICECandidate(String peerId, String candidate);
+
+ void signalRemoteDisconnect(String peerId);
+
+ int countPeers();
+
+ @JSFunctor
+ public static interface ICECandidateHandler extends JSObject {
+ void call(String peerId, String candidate);
+ }
+
+ @JSFunctor
+ public static interface DescriptionHandler extends JSObject {
+ void call(String peerId, String candidate);
+ }
+
+ @JSFunctor
+ public static interface ClientSignalHandler extends JSObject {
+ void call(String peerId);
+ }
+
+ @JSFunctor
+ public static interface PeerPacketHandler extends JSObject {
+ void call(String peerId, ArrayBuffer buffer);
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftVoiceClient.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftVoiceClient.java
new file mode 100644
index 0000000..cb2ba41
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/EaglercraftVoiceClient.java
@@ -0,0 +1,63 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.webaudio.MediaStream;
+
+public interface EaglercraftVoiceClient extends JSObject {
+
+ int READYSTATE_NONE = 0;
+ int READYSTATE_ABORTED = -1;
+ int READYSTATE_DEVICE_INITIALIZED = 1;
+
+ boolean voiceClientSupported();
+
+ void initializeDevices();
+
+ void setICEServers(String[] urls);
+
+ void setICECandidateHandler(ICECandidateHandler callback);
+
+ void setDescriptionHandler(DescriptionHandler callback);
+
+ void setPeerTrackHandler(PeerTrackHandler callback);
+
+ void setPeerDisconnectHandler(PeerDisconnectHandler callback);
+
+ void activateVoice(boolean active);
+
+ void setMicVolume(float volume);
+
+ void mutePeer(String peerId, boolean muted);
+
+ int getReadyState();
+
+ int signalConnect(String peerId, boolean offer);
+
+ int signalDescription(String peerId, String description);
+
+ int signalDisconnect(String peerId, boolean quiet);
+
+ int signalICECandidate(String peerId, String candidate);
+
+ @JSFunctor
+ public static interface ICECandidateHandler extends JSObject {
+ void call(String peerId, String candidate);
+ }
+
+ @JSFunctor
+ public static interface DescriptionHandler extends JSObject {
+ void call(String peerId, String candidate);
+ }
+
+ @JSFunctor
+ public static interface PeerTrackHandler extends JSObject {
+ void call(String peerId, MediaStream audioNode);
+ }
+
+ @JSFunctor
+ public static interface PeerDisconnectHandler extends JSObject {
+ void call(String peerId, boolean quiet);
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java
new file mode 100644
index 0000000..193c0cb
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/MessageChannel.java
@@ -0,0 +1,36 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.JSProperty;
+import org.teavm.jso.workers.MessagePort;
+
+/**
+ * Copyright (c) 2024 lax1dude. All Rights Reserved.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+@JSClass
+public class MessageChannel implements JSObject {
+
+ @JSBody(params = { }, script = "return (typeof MessageChannel !== \"undefined\");")
+ public static native boolean supported();
+
+ @JSProperty
+ public native MessagePort getPort1();
+
+ @JSProperty
+ public native MessagePort getPort2();
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/SelfDefence.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/SelfDefence.java
new file mode 100644
index 0000000..3a2b146
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/SelfDefence.java
@@ -0,0 +1,103 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.browser.TimerHandler;
+import org.teavm.jso.browser.Window;
+import org.teavm.jso.core.JSArrayReader;
+import org.teavm.jso.dom.html.HTMLCanvasElement;
+import org.teavm.jso.dom.html.HTMLDocument;
+import org.teavm.jso.dom.html.HTMLIFrameElement;
+import org.teavm.jso.dom.xml.Element;
+import org.teavm.jso.dom.xml.NodeList;
+
+public class SelfDefence {
+
+ private static HTMLCanvasElement canvas = null;
+ private static boolean ignoreNextWindow = false;
+
+ @JSFunctor
+ private static interface NewWindowCallback extends JSObject {
+ void call(Window newWindow);
+ }
+
+ @JSBody(params = { "cb" }, script = "const ccb = cb; const _open = window.open; window.open = (url,name,params) => { var rw = _open(url,name,params); ccb(rw); return rw; }")
+ private static native void injectWindowCapture(NewWindowCallback callback);
+
+ private static final List capturedChildWindows = new ArrayList<>();
+
+ public static void init(HTMLCanvasElement legitCanvas) {
+ canvas = legitCanvas;
+ for(int i = 0; i < 15; ++i) {
+ Window.setTimeout(new TimerHandler() {
+
+ @Override
+ public void onTimer() {
+ Window.setTimeout(this, (long)(Math.random() * 25000l));
+ run(Window.current());
+ for(int i = 0, l = capturedChildWindows.size(); i < l; ++i) {
+ run(capturedChildWindows.get(i));
+ }
+ }
+
+ }, (long)(Math.random() * 25000l));
+ }
+ injectWindowCapture(new NewWindowCallback() {
+ @Override
+ public void call(Window newWindow) {
+ if(!ignoreNextWindow) {
+ capturedChildWindows.add(newWindow);
+ }
+ ignoreNextWindow = false;
+ }
+ });
+ }
+
+ public static void openWindowIgnore(String url, String name) {
+ ignoreNextWindow = true;
+ Window.current().open(url, name);
+ }
+
+ private static void run(Window win) {
+ try {
+ run0(win);
+ }catch(Throwable t) {
+ }
+ }
+
+ private static void run0(Window win) {
+ run(win.getDocument());
+ JSArrayReader frms = win.getFrames();
+ for(int i = 0, l = frms.getLength(); i < l; ++i) {
+ HTMLIFrameElement frm = frms.get(i);
+ if(checkFrame(frm)) {
+ run(frm.getContentWindow());
+ }
+ }
+ }
+
+ @JSBody(params = { "frm" }, script = "try { var g = frm.contentWindow; g[\"fuck_off\"] = \"dick\"; return g[\"fuck_off\"] === \"dick\"; } catch (e) { return false; }")
+ private static native boolean checkFrame(HTMLIFrameElement frame);
+
+ private static void run(HTMLDocument doc) {
+ try {
+ run0(doc);
+ }catch(Throwable t) {
+ }
+ }
+
+ private static void run0(HTMLDocument doc) {
+ NodeList els = doc.getElementsByTagName("canvas");
+ for(int i = 0, l = els.getLength(); i < l; ++i) {
+ HTMLCanvasElement canv = (HTMLCanvasElement) els.get(i);
+ if(canvas != canv) {
+ canv.delete();
+ }
+ }
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/TeaVMUtils.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/TeaVMUtils.java
new file mode 100644
index 0000000..5fb0005
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/TeaVMUtils.java
@@ -0,0 +1,95 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.backend.javascript.spi.GeneratedBy;
+import org.teavm.backend.javascript.spi.InjectedBy;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+import org.teavm.jso.typedarrays.ArrayBufferView;
+import org.teavm.jso.typedarrays.Float32Array;
+import org.teavm.jso.typedarrays.Int16Array;
+import org.teavm.jso.typedarrays.Int32Array;
+import org.teavm.jso.typedarrays.Int8Array;
+import org.teavm.jso.typedarrays.Uint8Array;
+
+import net.lax1dude.eaglercraft.adapter.teavm.generators.TeaVMUtilsUnwrapGenerator;
+
+public class TeaVMUtils {
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native Int8Array unwrapByteArray(byte[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapArrayBuffer.class)
+ public static native ArrayBuffer unwrapArrayBuffer(byte[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native ArrayBufferView unwrapArrayBufferView(byte[] buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapTypedArray.class)
+ public static native byte[] wrapByteArray(Int8Array buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBuffer.class)
+ public static native byte[] wrapByteArrayBuffer(ArrayBuffer buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBufferView.class)
+ public static native byte[] wrapByteArrayBufferView(ArrayBufferView buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapUnsignedTypedArray.class)
+ public static native Uint8Array unwrapUnsignedByteArray(byte[] buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBufferView.class)
+ public static native byte[] wrapUnsignedByteArray(Uint8Array buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native Int32Array unwrapIntArray(int[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapArrayBuffer.class)
+ public static native ArrayBuffer unwrapArrayBuffer(int[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native ArrayBufferView unwrapArrayBufferView(int[] buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapTypedArray.class)
+ public static native int[] wrapIntArray(Int32Array buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBuffer.class)
+ public static native int[] wrapIntArrayBuffer(ArrayBuffer buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBufferView.class)
+ public static native int[] wrapIntArrayBufferView(ArrayBufferView buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native Float32Array unwrapFloatArray(float[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapArrayBuffer.class)
+ public static native ArrayBuffer unwrapArrayBuffer(float[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native ArrayBufferView unwrapArrayBufferView(float[] buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapTypedArray.class)
+ public static native float[] wrapFloatArray(Float32Array buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBuffer.class)
+ public static native float[] wrapFloatArrayBuffer(ArrayBuffer buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBufferView.class)
+ public static native float[] wrapFloatArrayBufferView(ArrayBufferView buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native Int16Array unwrapShortArray(short[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapArrayBuffer.class)
+ public static native ArrayBuffer unwrapArrayBuffer(short[] buf);
+
+ @InjectedBy(TeaVMUtilsUnwrapGenerator.UnwrapTypedArray.class)
+ public static native ArrayBufferView unwrapArrayBufferView(short[] buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapTypedArray.class)
+ public static native short[] wrapShortArray(Int16Array buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBuffer.class)
+ public static native short[] wrapShortArrayBuffer(ArrayBuffer buf);
+
+ @GeneratedBy(TeaVMUtilsUnwrapGenerator.WrapArrayBufferView.class)
+ public static native short[] wrapShortArrayBuffer(ArrayBufferView buf);
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGL2RenderingContext.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGL2RenderingContext.java
new file mode 100644
index 0000000..c185af1
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGL2RenderingContext.java
@@ -0,0 +1,46 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.webgl.WebGLRenderingContext;
+
+public interface WebGL2RenderingContext extends WebGLRenderingContext {
+
+ int TEXTURE_MAX_LEVEL = 0x0000813D;
+ int TEXTURE_MAX_ANISOTROPY_EXT = 0x000084FE;
+ int UNSIGNED_INT_24_8 = 0x000084FA;
+ int ANY_SAMPLES_PASSED = 0x00008D6A;
+ int QUERY_RESULT = 0x00008866;
+ int QUERY_RESULT_AVAILABLE = 0x00008867;
+ int DEPTH24_STENCIL8 = 0x000088F0;
+ int DEPTH_COMPONENT32F = 0x00008CAC;
+ int READ_FRAMEBUFFER = 0x00008CA8;
+ int DRAW_FRAMEBUFFER = 0x00008CA9;
+ int RGB8 = 0x00008051;
+ int RGBA8 = 0x00008058;
+ int R8 = 0x00008229;
+ int RED = 0x00001903;
+
+ WebGLQuery createQuery();
+
+ void beginQuery(int p1, WebGLQuery obj);
+
+ void endQuery(int p1);
+
+ void deleteQuery(WebGLQuery obj);
+
+ int getQueryParameter(WebGLQuery obj, int p2);
+
+ WebGLVertexArray createVertexArray();
+
+ void deleteVertexArray(WebGLVertexArray obj);
+
+ void bindVertexArray(WebGLVertexArray obj);
+
+ void renderbufferStorageMultisample(int p1, int p2, int p3, int p4, int p5);
+
+ void blitFramebuffer(int p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8, int p9, int p10);
+
+ void drawBuffers(int[] p1);
+
+ void readBuffer(int p1);
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLQuery.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLQuery.java
new file mode 100644
index 0000000..5b3acaa
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLQuery.java
@@ -0,0 +1,6 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSObject;
+
+public interface WebGLQuery extends JSObject {
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLVertexArray.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLVertexArray.java
new file mode 100644
index 0000000..467ffc1
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/WebGLVertexArray.java
@@ -0,0 +1,6 @@
+package net.lax1dude.eaglercraft.adapter.teavm;
+
+import org.teavm.jso.JSObject;
+
+public interface WebGLVertexArray extends JSObject {
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/generators/TeaVMUtilsUnwrapGenerator.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/generators/TeaVMUtilsUnwrapGenerator.java
new file mode 100644
index 0000000..c30672f
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/teavm/generators/TeaVMUtilsUnwrapGenerator.java
@@ -0,0 +1,158 @@
+package net.lax1dude.eaglercraft.adapter.teavm.generators;
+
+import org.teavm.backend.javascript.codegen.SourceWriter;
+import org.teavm.backend.javascript.spi.Generator;
+import org.teavm.backend.javascript.spi.GeneratorContext;
+import org.teavm.backend.javascript.spi.Injector;
+import org.teavm.backend.javascript.spi.InjectorContext;
+import org.teavm.model.MethodReference;
+
+public class TeaVMUtilsUnwrapGenerator {
+
+ // WARNING: This code uses internal TeaVM APIs that may not have
+ // been intended for end users of the compiler to program with
+
+ public static class UnwrapArrayBuffer implements Injector {
+
+ @Override
+ public void generate(InjectorContext context, MethodReference methodRef) {
+ context.writeExpr(context.getArgument(0));
+ context.getWriter().append(".data.buffer");
+ }
+
+ }
+
+ public static class UnwrapTypedArray implements Injector {
+
+ @Override
+ public void generate(InjectorContext context, MethodReference methodRef) {
+ context.writeExpr(context.getArgument(0));
+ context.getWriter().append(".data");
+ }
+
+ }
+
+ public static class WrapArrayBuffer implements Generator {
+
+ @Override
+ public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) {
+ String parName = context.getParameterName(1);
+ switch (methodRef.getName()) {
+ case "wrapByteArrayBuffer":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_bytecls").append(',').ws();
+ writer.append("new Int8Array(").append(parName).append("))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapIntArrayBuffer":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_intcls").append(',').ws();
+ writer.append("new Int32Array(").append(parName).append("))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapFloatArrayBuffer":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_floatcls").append(',').ws();
+ writer.append("new Float32Array(").append(parName).append("))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapShortArrayBuffer":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_shortcls").append(',').ws();
+ writer.append("new Int16Array(").append(parName).append("))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+
+ public static class WrapArrayBufferView implements Generator {
+
+ @Override
+ public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) {
+ String parName = context.getParameterName(1);
+ switch (methodRef.getName()) {
+ case "wrapByteArrayBufferView":
+ case "wrapUnsignedByteArray":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_bytecls").append(',').ws();
+ writer.append("new Int8Array(").append(parName).append(".buffer))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapIntArrayBufferView":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_intcls").append(',').ws();
+ writer.append("new Int32Array(").append(parName).append(".buffer))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapFloatArrayBufferView":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_floatcls").append(',').ws();
+ writer.append("new Float32Array(").append(parName).append(".buffer))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapShortArrayBufferView":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_shortcls").append(',').ws();
+ writer.append("new Int16Array(").append(parName).append(".buffer))").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+
+ public static class WrapTypedArray implements Generator {
+
+ @Override
+ public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) {
+ String parName = context.getParameterName(1);
+ switch (methodRef.getName()) {
+ case "wrapByteArray":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_shortcls").append(',').ws();
+ writer.append(parName).append(")").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapIntArray":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_intcls").append(',').ws();
+ writer.append(parName).append(")").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapFloatArray":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_floatcls").append(',').ws();
+ writer.append(parName).append(")").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ case "wrapShortArray":
+ writer.append("return ").append(parName).ws().append('?').ws();
+ writer.appendFunction("$rt_wrapArray").append('(').appendFunction("$rt_shortcls").append(',').ws();
+ writer.append(parName).append(")").ws();
+ writer.append(':').ws().append("null;").softNewLine();
+ break;
+ default:
+ break;
+ }
+ }
+
+ }
+
+ public static class UnwrapUnsignedTypedArray implements Injector {
+
+ @Override
+ public void generate(InjectorContext context, MethodReference methodRef) {
+ context.getWriter().append("new Uint8Array(");
+ context.writeExpr(context.getArgument(0));
+ context.getWriter().append(".data.buffer)");
+ }
+
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/BooleanResult.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/BooleanResult.java
new file mode 100644
index 0000000..a956837
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/BooleanResult.java
@@ -0,0 +1,18 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+public class BooleanResult {
+
+ public static final BooleanResult TRUE = new BooleanResult(true);
+ public static final BooleanResult FALSE = new BooleanResult(false);
+
+ public final boolean bool;
+
+ private BooleanResult(boolean b) {
+ bool = b;
+ }
+
+ public static BooleanResult _new(boolean b) {
+ return b ? TRUE : FALSE;
+ }
+
+}
\ No newline at end of file
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/SYS.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/SYS.java
new file mode 100644
index 0000000..303b084
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/SYS.java
@@ -0,0 +1,19 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+public class SYS {
+
+ public static final VirtualFilesystem VFS;
+
+ static {
+
+ VirtualFilesystem.VFSHandle vh = VirtualFilesystem.openVFS("eagStorage2");
+
+ if(vh.vfs == null) {
+ System.err.println("Could not init filesystem!");
+ throw new RuntimeException("Could not init filesystem: VFSHandle.vfs was null");
+ }
+
+ VFS = vh.vfs;
+
+ }
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFSIterator.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFSIterator.java
new file mode 100644
index 0000000..34b3d0e
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFSIterator.java
@@ -0,0 +1,17 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+public interface VFSIterator {
+
+ public static class BreakLoop extends RuntimeException {
+ public BreakLoop() {
+ super("iterator loop break request");
+ }
+ }
+
+ public default void end() {
+ throw new BreakLoop();
+ }
+
+ public void next(VIteratorFile entry);
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFile.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFile.java
new file mode 100644
index 0000000..857a5db
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VFile.java
@@ -0,0 +1,227 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class VFile {
+
+ public static final String pathSeperator = "/";
+ public static final String[] altPathSeperator = new String[] { "\\" };
+
+ public static String normalizePath(String p) {
+ for(int i = 0; i < altPathSeperator.length; ++i) {
+ p = p.replace(altPathSeperator[i], pathSeperator);
+ }
+ if(p.startsWith(pathSeperator)) {
+ p = p.substring(1);
+ }
+ if(p.endsWith(pathSeperator)) {
+ p = p.substring(0, p.length() - pathSeperator.length());
+ }
+ return p;
+ }
+
+ public static String[] splitPath(String p) {
+ String[] pth = normalizePath(p).split(pathSeperator);
+ for(int i = 0; i < pth.length; ++i) {
+ pth[i] = pth[i].trim();
+ }
+ return pth;
+ }
+
+ protected String path;
+
+ public static String createPath(Object... p) {
+ ArrayList r = new ArrayList<>();
+ for(int i = 0; i < p.length; ++i) {
+ if(p[i] == null) {
+ continue;
+ }
+ String gg = p[i].toString();
+ if(gg == null) {
+ continue;
+ }
+ String[] parts = splitPath(gg);
+ for(int j = 0; j < parts.length; ++j) {
+ if(parts[j] == null || parts[j].equals(".")) {
+ continue;
+ }else if(parts[j].equals("..") && r.size() > 0) {
+ int k = r.size() - 1;
+ if(!r.get(k).equals("..")) {
+ r.remove(k);
+ }else {
+ r.add("..");
+ }
+ }else {
+ r.add(parts[j]);
+ }
+ }
+ }
+ if(r.size() > 0) {
+ StringBuilder s = new StringBuilder();
+ for(int i = 0; i < r.size(); ++i) {
+ if(i > 0) {
+ s.append(pathSeperator);
+ }
+ s.append(r.get(i));
+ }
+ return s.toString();
+ }else {
+ return null;
+ }
+ }
+
+ public VFile(Object... p) {
+ this.path = createPath(p);
+ }
+
+ public InputStream getInputStream() {
+ return isRelative() ? null : SYS.VFS.getFile(path).getInputStream();
+ }
+
+ public OutputStream getOutputStream() {
+ return isRelative() ? null : SYS.VFS.getFile(path).getOutputStream();
+ }
+
+ public String toString() {
+ return path;
+ }
+
+ public boolean isRelative() {
+ return path == null || path.contains("..");
+ }
+
+ public boolean canRead() {
+ return !isRelative() && SYS.VFS.fileExists(path);
+ }
+
+ public String getPath() {
+ return path.equals("unnamed") ? null : path;
+ }
+
+ public String getName() {
+ if(path == null) {
+ return null;
+ }
+ int i = path.indexOf(pathSeperator);
+ return i == -1 ? path : path.substring(i + 1);
+ }
+
+ public boolean canWrite() {
+ return !isRelative();
+ }
+
+ public String getParent() {
+ if(path == null) {
+ return null;
+ }
+ int i = path.indexOf(pathSeperator);
+ return i == -1 ? ".." : path.substring(0, i);
+ }
+
+ public int hashCode() {
+ return path == null ? 0 : path.hashCode();
+ }
+
+ public boolean equals(Object o) {
+ return path != null && o != null && (o instanceof VFile) && path.equals(((VFile)o).path);
+ }
+
+ public boolean exists() {
+ return !isRelative() && SYS.VFS.fileExists(path);
+ }
+
+ public boolean delete() {
+ return !isRelative() && SYS.VFS.deleteFile(path);
+ }
+
+ public boolean renameTo(String p, boolean copy) {
+ if(!isRelative() && SYS.VFS.renameFile(path, p, copy)) {
+ path = p;
+ return true;
+ }
+ return false;
+ }
+
+ public int length() {
+ return isRelative() ? -1 : SYS.VFS.getFile(path).getSize();
+ }
+
+ public void getBytes(int fileOffset, byte[] array, int offset, int length) {
+ if(isRelative()) {
+ throw new ArrayIndexOutOfBoundsException("File is relative");
+ }
+ SYS.VFS.getFile(path).getBytes(fileOffset, array, offset, length);
+ }
+
+ public void setCacheEnabled() {
+ if(isRelative()) {
+ throw new RuntimeException("File is relative");
+ }
+ SYS.VFS.getFile(path).setCacheEnabled();
+ }
+
+ public byte[] getAllBytes() {
+ if(isRelative()) {
+ return null;
+ }
+ return SYS.VFS.getFile(path).getAllBytes();
+ }
+
+ public String getAllChars() {
+ if(isRelative()) {
+ return null;
+ }
+ return SYS.VFS.getFile(path).getAllChars();
+ }
+
+ public String[] getAllLines() {
+ if(isRelative()) {
+ return null;
+ }
+ return SYS.VFS.getFile(path).getAllLines();
+ }
+
+ public byte[] getAllBytes(boolean copy) {
+ if(isRelative()) {
+ return null;
+ }
+ return SYS.VFS.getFile(path).getAllBytes(copy);
+ }
+
+ public boolean setAllChars(String bytes) {
+ if(isRelative()) {
+ return false;
+ }
+ return SYS.VFS.getFile(path).setAllChars(bytes);
+ }
+
+ public boolean setAllBytes(byte[] bytes) {
+ if(isRelative()) {
+ return false;
+ }
+ return SYS.VFS.getFile(path).setAllBytes(bytes);
+ }
+
+ public boolean setAllBytes(byte[] bytes, boolean copy) {
+ if(isRelative()) {
+ return false;
+ }
+ return SYS.VFS.getFile(path).setAllBytes(bytes, copy);
+ }
+
+ public List list() {
+ if(isRelative()) {
+ return Arrays.asList(path);
+ }
+ return SYS.VFS.listFiles(path);
+ }
+
+ public int deleteAll() {
+ return isRelative() ? 0 : SYS.VFS.deleteFiles(path);
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VIteratorFile.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VIteratorFile.java
new file mode 100644
index 0000000..2dfa631
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VIteratorFile.java
@@ -0,0 +1,290 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.teavm.interop.Async;
+import org.teavm.interop.AsyncCallback;
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.dom.events.Event;
+import org.teavm.jso.dom.events.EventListener;
+import org.teavm.jso.indexeddb.IDBCursor;
+import org.teavm.jso.indexeddb.IDBRequest;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+import org.teavm.jso.typedarrays.Uint8Array;
+
+import net.lax1dude.eaglercraft.EaglerInputStream;
+
+/**
+ * Do not use an instance of this class outside of the VFSIterator.next() method
+ */
+public class VIteratorFile extends VFile {
+
+ static final VIteratorFile instance = new VIteratorFile();
+
+ private VIteratorFile() {
+ super("");
+ this.idx = -1;
+ this.cur = null;
+ this.vfs = null;
+ }
+
+ private static class VirtualIteratorOutputStream extends ByteArrayOutputStream {
+
+ private final VIteratorFile itr;
+
+ protected VirtualIteratorOutputStream(VIteratorFile itr) {
+ this.itr = itr;
+ }
+
+ public void close() throws IOException {
+ if(!itr.setAllBytes(super.toByteArray(), false)) {
+ throw new IOException("Could not close stream and write to \"" + itr.path + "\" on VFS \"" + itr.vfs.database + "\" (the file was probably deleted)");
+ }
+ }
+
+ }
+
+ private int idx;
+ private IDBCursor cur;
+ private VirtualFilesystem vfs;
+ private boolean wasDeleted;
+
+ @JSBody(params = { "k" }, script = "return ((typeof k) === \"string\") ? k : (((typeof k) === \"undefined\") ? null : (((typeof k[0]) === \"string\") ? k[0] : null));")
+ private static native String readKey(JSObject k);
+
+ static VIteratorFile create(int idx, VirtualFilesystem vfs, IDBCursor cur) {
+ String k = readKey(cur.getKey());
+ if(k == null) {
+ return null;
+ }
+ instance.update(idx, k, vfs, cur);
+ return instance;
+ }
+
+ public VFile makeVFile() {
+ return new VFile(path);
+ }
+
+ private void update(int idx, String path, VirtualFilesystem vfs, IDBCursor cur) {
+ this.idx = idx;
+ this.path = path;
+ this.vfs = vfs;
+ this.cur = cur;
+ this.wasDeleted = false;
+ }
+
+ public InputStream getInputStream() {
+ return !wasDeleted ? new EaglerInputStream(getAllBytes()) : null;
+ }
+
+ public OutputStream getOutputStream() {
+ return !wasDeleted ? new VirtualIteratorOutputStream(this) : null;
+ }
+
+ public String toString() {
+ return path;
+ }
+
+ public boolean isRelative() {
+ return false;
+ }
+
+ public boolean canRead() {
+ return !wasDeleted;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getName() {
+ if(path == null) {
+ return null;
+ }
+ int i = path.indexOf(pathSeperator);
+ return i == -1 ? path : path.substring(i + 1);
+ }
+
+ public boolean canWrite() {
+ return !wasDeleted;
+ }
+
+ public String getParent() {
+ if(path == null) {
+ return null;
+ }
+ int i = path.indexOf(pathSeperator);
+ return i == -1 ? ".." : path.substring(0, i);
+ }
+
+ public int hashCode() {
+ return path == null ? 0 : path.hashCode();
+ }
+
+ public boolean equals(Object o) {
+ return path != null && o != null && (o instanceof VFile) && path.equals(((VFile)o).path);
+ }
+
+ public boolean exists() {
+ return !wasDeleted;
+ }
+
+ public boolean delete() {
+ return wasDeleted = AsyncHandlers.awaitRequest(cur.delete()).bool;
+ }
+
+ public boolean renameTo(String p) {
+ byte[] data = getAllBytes();
+ String op = path;
+ path = p;
+ if(!setAllBytes(data)) {
+ path = op;
+ return false;
+ }
+ path = op;
+ if(!delete()) {
+ return false;
+ }
+ path = p;
+ return true;
+ }
+
+ public int length() {
+ JSObject obj = cur.getValue();
+
+ if(obj == null) {
+ throw new RuntimeException("Value of entry is missing");
+ }
+
+ ArrayBuffer arr = readRow(obj);
+
+ if(arr == null) {
+ throw new RuntimeException("Value of the fucking value of the entry is missing");
+ }
+
+ return arr.getByteLength();
+ }
+
+ public void getBytes(int fileOffset, byte[] array, int offset, int length) {
+ JSObject obj = cur.getValue();
+
+ if(obj == null) {
+ throw new ArrayIndexOutOfBoundsException("Value of entry is missing");
+ }
+
+ ArrayBuffer arr = readRow(obj);
+
+ if(arr == null) {
+ throw new ArrayIndexOutOfBoundsException("Value of the fucking value of the entry is missing");
+ }
+
+ Uint8Array a = new Uint8Array(arr);
+
+ if(a.getLength() < fileOffset + length) {
+ throw new ArrayIndexOutOfBoundsException("file '" + path + "' size was "+a.getLength()+" but user tried to read index "+(fileOffset + length - 1));
+ }
+
+ for(int i = 0; i < length; ++i) {
+ array[i + offset] = (byte)a.get(i + fileOffset);
+ }
+ }
+
+ public void setCacheEnabled() {
+ // no
+ }
+
+ @JSBody(params = { "obj" }, script = "return (typeof obj === 'undefined') ? null : ((typeof obj.data === 'undefined') ? null : obj.data);")
+ private static native ArrayBuffer readRow(JSObject obj);
+
+ public byte[] getAllBytes() {
+ JSObject obj = cur.getValue();
+
+ if(obj == null) {
+ return null;
+ }
+
+ ArrayBuffer arr = readRow(obj);
+
+ if(arr == null) {
+ return null;
+ }
+
+ Uint8Array a = new Uint8Array(arr);
+ int ii = a.getByteLength();
+
+ byte[] array = new byte[ii];
+ for(int i = 0; i < ii; ++i) {
+ array[i] = (byte)a.get(i);
+ }
+
+ return array;
+ }
+
+ public String getAllChars() {
+ return VirtualFilesystem.utf8(getAllBytes());
+ }
+
+ public String[] getAllLines() {
+ return VirtualFilesystem.lines(VirtualFilesystem.utf8(getAllBytes()));
+ }
+
+ public byte[] getAllBytes(boolean copy) {
+ return getAllBytes();
+ }
+
+ public boolean setAllChars(String bytes) {
+ return setAllBytes(VirtualFilesystem.utf8(bytes));
+ }
+
+ public List list() {
+ throw new RuntimeException("Cannot perform list all in VFS callback");
+ }
+
+ public int deleteAll() {
+ throw new RuntimeException("Cannot perform delete all in VFS callback");
+ }
+
+ @JSBody(params = { "pat", "dat" }, script = "return { path: pat, data: dat };")
+ private static native JSObject writeRow(String name, ArrayBuffer data);
+
+ public boolean setAllBytes(byte[] bytes) {
+ ArrayBuffer a = new ArrayBuffer(bytes.length);
+ Uint8Array ar = new Uint8Array(a);
+ ar.set(bytes);
+ JSObject obj = writeRow(path, a);
+ BooleanResult r = AsyncHandlers.awaitRequest(cur.update(obj));
+ return r.bool;
+ }
+
+ public boolean setAllBytes(byte[] bytes, boolean copy) {
+ return setAllBytes(bytes);
+ }
+
+ public static class AsyncHandlers {
+
+ @Async
+ public static native BooleanResult awaitRequest(IDBRequest r);
+
+ private static void awaitRequest(IDBRequest r, final AsyncCallback cb) {
+ r.addEventListener("success", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ cb.complete(BooleanResult._new(true));
+ }
+ });
+ r.addEventListener("error", new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ cb.complete(BooleanResult._new(false));
+ }
+ });
+ }
+
+ }
+
+}
diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VirtualFilesystem.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VirtualFilesystem.java
new file mode 100644
index 0000000..1195923
--- /dev/null
+++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/vfs/VirtualFilesystem.java
@@ -0,0 +1,690 @@
+package net.lax1dude.eaglercraft.adapter.vfs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import net.lax1dude.eaglercraft.EaglerAdapter;
+import net.lax1dude.eaglercraft.EaglerInputStream;
+import net.lax1dude.eaglercraft.adapter.teavm.TeaVMUtils;
+
+import org.teavm.interop.Async;
+import org.teavm.interop.AsyncCallback;
+import org.teavm.jso.JSBody;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.dom.events.EventListener;
+import org.teavm.jso.indexeddb.EventHandler;
+import org.teavm.jso.indexeddb.IDBCountRequest;
+import org.teavm.jso.indexeddb.IDBCursor;
+import org.teavm.jso.indexeddb.IDBCursorRequest;
+import org.teavm.jso.indexeddb.IDBDatabase;
+import org.teavm.jso.indexeddb.IDBFactory;
+import org.teavm.jso.indexeddb.IDBGetRequest;
+import org.teavm.jso.indexeddb.IDBObjectStoreParameters;
+import org.teavm.jso.indexeddb.IDBOpenDBRequest;
+import org.teavm.jso.indexeddb.IDBRequest;
+import org.teavm.jso.indexeddb.IDBTransaction;
+import org.teavm.jso.indexeddb.IDBVersionChangeEvent;
+import org.teavm.jso.typedarrays.ArrayBuffer;
+import org.teavm.jso.typedarrays.Int8Array;
+
+public class VirtualFilesystem {
+
+ protected static class VirtualOutputStream extends ByteArrayOutputStream {
+ private final VFSFile file;
+
+ protected VirtualOutputStream(VFSFile file) {
+ this.file = file;
+ }
+
+ public void close() throws IOException {
+ if(!file.setAllBytes(super.toByteArray(), false)) {
+ throw new IOException("Could not close stream and write to \"" + file.filePath + "\" on VFS \"" + file.virtualFilesystem.database + "\" (the file was probably deleted)");
+ }
+ }
+ }
+
+ public static class VFSFile {
+
+ public final VirtualFilesystem virtualFilesystem;
+ protected boolean cacheEnabled;
+ protected String filePath;
+ protected int fileSize = -1;
+ protected boolean hasBeenDeleted = false;
+ protected boolean hasBeenAccessed = false;
+ protected boolean exists = false;
+
+ protected byte[] cache = null;
+ protected long cacheHit;
+
+ protected VFSFile(VirtualFilesystem vfs, String filePath, boolean cacheEnabled) {
+ this.virtualFilesystem = vfs;
+ this.filePath = filePath;
+ this.cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(cacheEnabled) {
+ setCacheEnabled();
+ }
+ }
+
+ public boolean equals(Object o) {
+ return (o instanceof VFSFile) && ((VFSFile)o).filePath.equals(filePath);
+ }
+
+ public int hashCode() {
+ return filePath.hashCode();
+ }
+
+ public String getPath() {
+ return filePath;
+ }
+
+ public int getSize() {
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(fileSize < 0) {
+ if(cacheEnabled) {
+ byte[] b = getAllBytes(false);
+ if(b != null) {
+ fileSize = b.length;
+ }
+ }else {
+ ArrayBuffer dat = AsyncHandlers.readWholeFile(virtualFilesystem.indexeddb, filePath);
+ if(dat != null) {
+ fileSize = dat.getByteLength();
+ }
+ }
+ }
+ return fileSize;
+ }
+
+ public InputStream getInputStream() {
+ byte[] dat = getAllBytes(true);
+ if(dat == null) {
+ return null;
+ }
+ return new EaglerInputStream(dat);
+ }
+
+ public OutputStream getOutputStream() {
+ return new VirtualOutputStream(this);
+ }
+
+ public void getBytes(int fileOffset, byte[] array, int offset, int length) {
+ if(hasBeenDeleted) {
+ throw new ArrayIndexOutOfBoundsException("file '" + filePath + "' has been deleted");
+ }else if(hasBeenAccessed && !exists) {
+ throw new ArrayIndexOutOfBoundsException("file '" + filePath + "' does not exist");
+ }
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(cacheEnabled && cache != null) {
+ System.arraycopy(cache, fileOffset, array, offset, length);
+ }else {
+ ArrayBuffer aa = AsyncHandlers.readWholeFile(virtualFilesystem.indexeddb, filePath);
+ hasBeenAccessed = true;
+ if(aa != null) {
+ exists = true;
+ }else {
+ exists = false;
+ throw new ArrayIndexOutOfBoundsException("file '" + filePath + "' does not exist");
+ }
+ this.fileSize = aa.getByteLength();
+ if(cacheEnabled) {
+ cache = TeaVMUtils.wrapByteArrayBuffer(aa);
+ }
+ if(fileSize < fileOffset + length) {
+ throw new ArrayIndexOutOfBoundsException("file '" + filePath + "' size was "+fileSize+" but user tried to read index "+(fileOffset + length - 1));
+ }
+ TeaVMUtils.unwrapByteArray(array).set(new Int8Array(aa, fileOffset, length), offset);
+ }
+ }
+
+ public void setCacheEnabled() {
+ if(!cacheEnabled && !hasBeenDeleted && !(hasBeenAccessed && !exists)) {
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ cache = getAllBytes(false);
+ cacheEnabled = true;
+ }
+ }
+
+ public byte[] getAllBytes() {
+ return getAllBytes(false);
+ }
+
+ public String getAllChars() {
+ return utf8(getAllBytes(false));
+ }
+
+ public String[] getAllLines() {
+ return lines(getAllChars());
+ }
+
+ public byte[] getAllBytes(boolean copy) {
+ if(hasBeenDeleted || (hasBeenAccessed && !exists)) {
+ return null;
+ }
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(cacheEnabled && cache != null) {
+ byte[] b = cache;
+ if(copy) {
+ b = new byte[cache.length];
+ System.arraycopy(cache, 0, b, 0, cache.length);
+ }
+ return b;
+ }else {
+ hasBeenAccessed = true;
+ ArrayBuffer b = AsyncHandlers.readWholeFile(virtualFilesystem.indexeddb, filePath);
+ if(b != null) {
+ exists = true;
+ }else {
+ exists = false;
+ return null;
+ }
+ this.fileSize = b.getByteLength();
+ if(cacheEnabled) {
+ if(copy) {
+ cache = new byte[fileSize];
+ TeaVMUtils.unwrapByteArray(cache).set(new Int8Array(b));
+ }else {
+ cache = TeaVMUtils.wrapByteArrayBuffer(b);
+ }
+ }
+ return TeaVMUtils.wrapByteArrayBuffer(b);
+ }
+ }
+
+ public boolean setAllChars(String bytes) {
+ return setAllBytes(utf8(bytes), true);
+ }
+
+ public boolean setAllBytes(byte[] bytes) {
+ return setAllBytes(bytes, true);
+ }
+
+ public boolean setAllBytes(byte[] bytes, boolean copy) {
+ if(hasBeenDeleted || bytes == null) {
+ return false;
+ }
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ this.fileSize = bytes.length;
+ if(cacheEnabled) {
+ byte[] copz = bytes;
+ if(copy) {
+ copz = new byte[bytes.length];
+ System.arraycopy(bytes, 0, copz, 0, bytes.length);
+ }
+ cache = copz;
+ return sync();
+ }else {
+ boolean s = AsyncHandlers.writeWholeFile(virtualFilesystem.indexeddb, filePath,
+ TeaVMUtils.unwrapArrayBuffer(bytes)).bool;
+ hasBeenAccessed = true;
+ exists = exists || s;
+ return s;
+ }
+ }
+
+ public boolean sync() {
+ if(cacheEnabled && cache != null && !hasBeenDeleted) {
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ boolean tryWrite = AsyncHandlers.writeWholeFile(virtualFilesystem.indexeddb, filePath,
+ TeaVMUtils.unwrapArrayBuffer(cache)).bool;
+ hasBeenAccessed = true;
+ exists = exists || tryWrite;
+ return tryWrite;
+ }
+ return false;
+ }
+
+ public boolean delete() {
+ if(!hasBeenDeleted && !(hasBeenAccessed && !exists)) {
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(!AsyncHandlers.deleteFile(virtualFilesystem.indexeddb, filePath).bool) {
+ hasBeenAccessed = true;
+ return false;
+ }
+ virtualFilesystem.fileMap.remove(filePath);
+ hasBeenDeleted = true;
+ hasBeenAccessed = true;
+ exists = false;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean rename(String newName, boolean copy) {
+ if(!hasBeenDeleted && !(hasBeenAccessed && !exists)) {
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ ArrayBuffer arr = AsyncHandlers.readWholeFile(virtualFilesystem.indexeddb, filePath);
+ hasBeenAccessed = true;
+ if(arr != null) {
+ exists = true;
+ if(!AsyncHandlers.writeWholeFile(virtualFilesystem.indexeddb, newName, arr).bool) {
+ return false;
+ }
+ if(!copy && !AsyncHandlers.deleteFile(virtualFilesystem.indexeddb, filePath).bool) {
+ return false;
+ }
+ }else {
+ exists = false;
+ }
+ if(!copy) {
+ virtualFilesystem.fileMap.remove(filePath);
+ filePath = newName;
+ virtualFilesystem.fileMap.put(newName, this);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public boolean exists() {
+ if(hasBeenDeleted) {
+ return false;
+ }
+ cacheHit = EaglerAdapter.steadyTimeMillis();
+ if(hasBeenAccessed) {
+ return exists;
+ }
+ exists = AsyncHandlers.fileExists(virtualFilesystem.indexeddb, filePath).bool;
+ hasBeenAccessed = true;
+ return exists;
+ }
+
+ }
+
+ private final HashMap fileMap = new HashMap<>();
+
+ public final String database;
+ private final IDBDatabase indexeddb;
+
+ public static class VFSHandle {
+
+ public final boolean failedInit;
+ public final boolean failedLocked;
+ public final String failedError;
+ public final VirtualFilesystem vfs;
+
+ public VFSHandle(boolean init, boolean locked, String error, VirtualFilesystem db) {
+ failedInit = init;
+ failedLocked = locked;
+ failedError = error;
+ vfs = db;
+ }
+
+ public String toString() {
+ if(failedInit) {
+ return "IDBFactory threw an exception, IndexedDB is most likely not supported in this browser." + (failedError == null ? "" : "\n\n" + failedError);
+ }
+ if(failedLocked) {
+ return "The filesystem requested is already in use on a different tab.";
+ }
+ if(failedError != null) {
+ return "The IDBFactory.open() request failed, reason: " + failedError;
+ }
+ return "Virtual Filesystem Object: " + vfs.database;
+ }
+
+ }
+
+ public static VFSHandle openVFS(String db) {
+ DatabaseOpen evt = AsyncHandlers.openDB(db);
+ if(evt.failedInit) {
+ return new VFSHandle(true, false, evt.failedError, null);
+ }
+ if(evt.failedLocked) {
+ return new VFSHandle(false, true, null, null);
+ }
+ if(evt.failedError != null) {
+ return new VFSHandle(false, false, evt.failedError, null);
+ }
+ return new VFSHandle(false, false, null, new VirtualFilesystem(db, evt.database));
+ }
+
+ private VirtualFilesystem(String db, IDBDatabase idb) {
+ database = db;
+ indexeddb = idb;
+ }
+
+ public void close() {
+ indexeddb.close();
+ }
+
+ public VFSFile getFile(String path) {
+ return getFile(path, false);
+ }
+
+ public VFSFile getFile(String path, boolean cache) {
+ VFSFile f = fileMap.get(path);
+ if(f == null) {
+ fileMap.put(path, f = new VFSFile(this, path, cache));
+ }else {
+ if(cache) {
+ f.setCacheEnabled();
+ }
+ }
+ return f;
+ }
+
+ public boolean renameFile(String oldName, String newName, boolean copy) {
+ return getFile(oldName).rename(newName, copy);
+ }
+
+ public boolean deleteFile(String path) {
+ return getFile(path).delete();
+ }
+
+ public boolean fileExists(String path) {
+ return getFile(path).exists();
+ }
+
+ public List listFiles(String prefix) {
+ final ArrayList list = new ArrayList<>();
+ AsyncHandlers.iterateFiles(indexeddb, this, prefix, false, (v) -> {
+ list.add(v.getPath());
+ });
+ return list;
+ }
+
+ public List listVFiles(String prefix) {
+ final ArrayList list = new ArrayList<>();
+ AsyncHandlers.iterateFiles(indexeddb, this, prefix, false, (v) -> {
+ list.add(new VFile(v.getPath()));
+ });
+ return list;
+ }
+
+ public int deleteFiles(String prefix) {
+ return AsyncHandlers.deleteFiles(indexeddb, prefix);
+ }
+
+ public int iterateFiles(String prefix, boolean rw, VFSIterator itr) {
+ return AsyncHandlers.iterateFiles(indexeddb, this, prefix, rw, itr);
+ }
+
+ public int renameFiles(String oldPrefix, String newPrefix, boolean copy) {
+ List filesToCopy = listFiles(oldPrefix);
+ int i = 0;
+ for(String str : filesToCopy) {
+ String f = VFile.createPath(newPrefix, str.substring(oldPrefix.length()));
+ if(!renameFile(str, f, copy)) {
+ System.err.println("Could not " + (copy ? "copy" : "rename") + " file \"" + str + "\" to \"" + f + "\" for some reason");
+ }else {
+ ++i;
+ }
+ }
+ return i;
+ }
+
+ public void flushCache(long age) {
+ long curr = EaglerAdapter.steadyTimeMillis();
+ Iterator files = fileMap.values().iterator();
+ while(files.hasNext()) {
+ if(curr - files.next().cacheHit > age) {
+ files.remove();
+ }
+ }
+ }
+
+ protected static class DatabaseOpen {
+
+ protected final boolean failedInit;
+ protected final boolean failedLocked;
+ protected final String failedError;
+
+ protected final IDBDatabase database;
+
+ protected DatabaseOpen(boolean init, boolean locked, String error, IDBDatabase db) {
+ failedInit = init;
+ failedLocked = locked;
+ failedError = error;
+ database = db;
+ }
+
+ }
+
+ @JSBody(script = "return ((typeof indexedDB) !== 'undefined') ? indexedDB : null;")
+ protected static native IDBFactory createIDBFactory();
+
+ protected static class AsyncHandlers {
+
+ @Async
+ protected static native DatabaseOpen openDB(String name);
+
+ private static void openDB(String name, final AsyncCallback cb) {
+ IDBFactory i = createIDBFactory();
+ if(i == null) {
+ cb.complete(new DatabaseOpen(false, false, "window.indexedDB was null or undefined", null));
+ return;
+ }
+ final IDBOpenDBRequest f = i.open(name, 1);
+ f.setOnBlocked(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(new DatabaseOpen(false, true, null, null));
+ }
+ });
+ f.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(new DatabaseOpen(false, false, null, f.getResult()));
+ }
+ });
+ f.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(new DatabaseOpen(false, false, "open error", null));
+ }
+ });
+ f.setOnUpgradeNeeded(new EventListener() {
+ @Override
+ public void handleEvent(IDBVersionChangeEvent evt) {
+ f.getResult().createObjectStore("filesystem", IDBObjectStoreParameters.create().keyPath("path"));
+ }
+ });
+ }
+
+ @Async
+ protected static native BooleanResult deleteFile(IDBDatabase db, String name);
+
+ private static void deleteFile(IDBDatabase db, String name, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", "readwrite");
+ final IDBRequest r = tx.objectStore("filesystem").delete(makeTheFuckingKeyWork(name));
+
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(true));
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(false));
+ }
+ });
+ }
+
+ @JSBody(params = { "obj" }, script = "return (typeof obj === 'undefined') ? null : ((typeof obj.data === 'undefined') ? null : obj.data);")
+ protected static native ArrayBuffer readRow(JSObject obj);
+
+ @JSBody(params = { "obj" }, script = "return [obj];")
+ private static native JSObject makeTheFuckingKeyWork(String k);
+
+ @Async
+ protected static native ArrayBuffer readWholeFile(IDBDatabase db, String name);
+
+ private static void readWholeFile(IDBDatabase db, String name, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", "readonly");
+ final IDBGetRequest r = tx.objectStore("filesystem").get(makeTheFuckingKeyWork(name));
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(readRow(r.getResult()));
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(null);
+ }
+ });
+
+ }
+
+ @JSBody(params = { "k" }, script = "return ((typeof k) === \"string\") ? k : (((typeof k) === \"undefined\") ? null : (((typeof k[0]) === \"string\") ? k[0] : null));")
+ private static native String readKey(JSObject k);
+
+ @JSBody(params = { "k" }, script = "return ((typeof k) === \"undefined\") ? null : (((typeof k.path) === \"undefined\") ? null : (((typeof k.path) === \"string\") ? k[0] : null));")
+ private static native String readRowKey(JSObject r);
+
+ @Async
+ protected static native Integer iterateFiles(IDBDatabase db, final VirtualFilesystem vfs, final String prefix, boolean rw, final VFSIterator itr);
+
+ private static void iterateFiles(IDBDatabase db, final VirtualFilesystem vfs, final String prefix, boolean rw, final VFSIterator itr, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", rw ? "readwrite" : "readonly");
+ final IDBCursorRequest r = tx.objectStore("filesystem").openCursor();
+ final int[] res = new int[1];
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ IDBCursor c = r.getResult();
+ if(c == null || c.getKey() == null || c.getValue() == null) {
+ cb.complete(res[0]);
+ return;
+ }
+ String k = readKey(c.getKey());
+ if(k != null) {
+ if(k.startsWith(prefix)) {
+ int ci = res[0]++;
+ try {
+ itr.next(VIteratorFile.create(ci, vfs, c));
+ }catch(VFSIterator.BreakLoop ex) {
+ cb.complete(res[0]);
+ return;
+ }
+ }
+ }
+ c.doContinue();
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(res[0] > 0 ? res[0] : -1);
+ }
+ });
+ }
+
+ @Async
+ protected static native Integer deleteFiles(IDBDatabase db, final String prefix);
+
+ private static void deleteFiles(IDBDatabase db, final String prefix, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", "readwrite");
+ final IDBCursorRequest r = tx.objectStore("filesystem").openCursor();
+ final int[] res = new int[1];
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ IDBCursor c = r.getResult();
+ if(c == null || c.getKey() == null || c.getValue() == null) {
+ cb.complete(res[0]);
+ return;
+ }
+ String k = readKey(c.getKey());
+ if(k != null) {
+ if(k.startsWith(prefix)) {
+ c.delete();
+ ++res[0];
+ }
+ }
+ c.doContinue();
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(res[0] > 0 ? res[0] : -1);
+ }
+ });
+ }
+
+ @Async
+ protected static native BooleanResult fileExists(IDBDatabase db, String name);
+
+ private static void fileExists(IDBDatabase db, String name, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", "readonly");
+ final IDBCountRequest r = tx.objectStore("filesystem").count(makeTheFuckingKeyWork(name));
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(r.getResult() > 0));
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(false));
+ }
+ });
+ }
+
+ @JSBody(params = { "pat", "dat" }, script = "return { path: pat, data: dat };")
+ protected static native JSObject writeRow(String name, ArrayBuffer data);
+
+ @Async
+ protected static native BooleanResult writeWholeFile(IDBDatabase db, String name, ArrayBuffer data);
+
+ private static void writeWholeFile(IDBDatabase db, String name, ArrayBuffer data, final AsyncCallback cb) {
+ IDBTransaction tx = db.transaction("filesystem", "readwrite");
+ final IDBRequest r = tx.objectStore("filesystem").put(writeRow(name, data));
+
+ r.setOnSuccess(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(true));
+ }
+ });
+ r.setOnError(new EventHandler() {
+ @Override
+ public void handleEvent() {
+ cb.complete(BooleanResult._new(false));
+ }
+ });
+ }
+
+ }
+
+ public static byte[] utf8(String str) {
+ if(str == null) return null;
+ return str.getBytes(Charset.forName("UTF-8"));
+ }
+
+ public static String utf8(byte[] str) {
+ if(str == null) return null;
+ return new String(str, Charset.forName("UTF-8"));
+ }
+
+ public static String CRLFtoLF(String str) {
+ if(str == null) return null;
+ str = str.indexOf('\r') != -1 ? str.replace("\r", "") : str;
+ str = str.trim();
+ if(str.endsWith("\n")) {
+ str = str.substring(0, str.length() - 1);
+ }
+ if(str.startsWith("\n")) {
+ str = str.substring(1);
+ }
+ return str;
+ }
+
+ public static String[] lines(String str) {
+ if(str == null) return null;
+ return CRLFtoLF(str).split("\n");
+ }
+
+}
\ No newline at end of file