Update #52 - Fixed various issues with the client

This commit is contained in:
lax1dude
2025-06-15 21:43:43 -07:00
parent 325a6826bf
commit f3281c037f
94 changed files with 882 additions and 506 deletions

View File

@ -27,6 +27,8 @@ 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.dom.html.HTMLAudioElement;
import org.teavm.jso.dom.html.HTMLSourceElement;
import org.teavm.jso.typedarrays.ArrayBuffer;
import org.teavm.jso.typedarrays.Int8Array;
import org.teavm.jso.webaudio.AudioBuffer;
@ -43,6 +45,7 @@ import org.teavm.jso.webaudio.PannerNode;
import net.lax1dude.eaglercraft.v1_8.EagRuntime;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.JOrbisAudioBufferDecoder;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMBlobURLManager;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMClientConfigAdapter;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMUtils;
import net.lax1dude.eaglercraft.v1_8.log4j.LogManager;
@ -243,7 +246,25 @@ public class PlatformAudio {
}
PlatformInput.clearEventBuffers();
if(((TeaVMClientConfigAdapter)PlatformRuntime.getClientConfigAdapter()).isKeepAliveHackTeaVM()) {
byte[] silenceFile = PlatformAssets.getResourceBytes("/assets/eagler/silence_loop.wav");
if (silenceFile != null) {
HTMLAudioElement audio = (HTMLAudioElement) PlatformRuntime.doc.createElement("audio");
audio.getClassList().add("_eaglercraftX_keepalive_hack");
audio.setAttribute("style", "display:none;");
audio.setAutoplay(true);
audio.setLoop(true);
HTMLSourceElement source = (HTMLSourceElement) PlatformRuntime.doc.createElement("source");
source.setType("audio/wav");
source.setSrc(TeaVMBlobURLManager.registerNewURLByte(silenceFile, "audio/wav").toExternalForm());
audio.appendChild(source);
audio.addEventListener("seeked", (e) -> {
// NOP, wakes up the browser's event loop
});
PlatformRuntime.parent.appendChild(audio);
}
}
}
@JSBody(params = { "ctx" }, script = "var tmpBuf = ctx.createBuffer(2, 16, 16000); return (typeof tmpBuf.copyToChannel === \"function\");")

View File

@ -58,6 +58,7 @@ import net.lax1dude.eaglercraft.v1_8.internal.teavm.InputEvent;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.LegacyKeycodeTranslator;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.OffsetTouch;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.SortedTouchEvent;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMClientConfigAdapter;
import net.lax1dude.eaglercraft.v1_8.internal.teavm.WebGLBackBuffer;
public class PlatformInput {
@ -241,13 +242,9 @@ public class PlatformInput {
static boolean vsync = true;
static boolean vsyncSupport = false;
static boolean finish = true;
private static long vsyncWaiting = -1l;
private static AsyncCallback<Void> vsyncAsyncCallback = null;
private static int vsyncTimeout = -1;
// hack to fix occasional freeze on iOS
private static int vsyncSaveLockInterval = -1;
@JSFunctor
private static interface UnloadCallback extends JSObject {
@ -723,8 +720,6 @@ public class PlatformInput {
}catch(Throwable t) {
}
vsyncWaiting = -1l;
vsyncAsyncCallback = null;
vsyncTimeout = -1;
vsyncSupport = false;
@ -735,39 +730,7 @@ public class PlatformInput {
PlatformRuntime.logger.error("VSync is not supported on this browser!");
}
if(vsyncSupport) {
if(vsyncSaveLockInterval != -1) {
try {
Window.clearInterval(vsyncSaveLockInterval);
}catch(Throwable t) {
}
vsyncSaveLockInterval = -1;
}
// fix for iOS freezing randomly...?
vsyncSaveLockInterval = Window.setInterval(() -> {
if(vsyncWaiting != -1l) {
long steadyTime = PlatformRuntime.steadyTimeMillis();
if(steadyTime - vsyncWaiting > 1000) {
PlatformRuntime.logger.error("VSync lockup detected! Attempting to recover...");
vsyncWaiting = -1l;
if(vsyncTimeout != -1) {
try {
Window.clearTimeout(vsyncTimeout);
}catch(Throwable t) {
}
vsyncTimeout = -1;
}
if(vsyncAsyncCallback != null) {
AsyncCallback<Void> cb = vsyncAsyncCallback;
vsyncAsyncCallback = null;
cb.complete(null);
}else {
PlatformRuntime.logger.error("Async callback is null!");
}
}
}
}, 1000);
}
finish = ((TeaVMClientConfigAdapter)PlatformRuntime.getClientConfigAdapter()).isFinishOnSwapTeaVM();
try {
gamepadSupported = gamepadSupported();
@ -968,6 +931,9 @@ public class PlatformInput {
syncTimer = 0.0;
asyncRequestAnimationFrame();
}else {
if(finish) {
PlatformOpenGL.ctx.finish();
}
if(fpsLimit <= 0 || fpsLimit > 1000) {
syncTimer = 0.0;
PlatformRuntime.swapDelayTeaVM();
@ -1010,38 +976,27 @@ public class PlatformInput {
private static native void asyncRequestAnimationFrame();
private static void asyncRequestAnimationFrame(AsyncCallback<Void> cb) {
if(vsyncWaiting != -1l) {
if(vsyncTimeout != -1) {
cb.error(new IllegalStateException("Already waiting for vsync!"));
return;
}
vsyncWaiting = PlatformRuntime.steadyTimeMillis();
vsyncAsyncCallback = cb;
final boolean[] hasTimedOut = new boolean[] { false };
final int[] timeout = new int[] { -1 };
Window.requestAnimationFrame((d) -> {
if(!hasTimedOut[0]) {
hasTimedOut[0] = true;
if(vsyncWaiting != -1l) {
vsyncWaiting = -1l;
if(vsyncTimeout != -1 && vsyncTimeout == timeout[0]) {
try {
Window.clearTimeout(vsyncTimeout);
}catch(Throwable t) {
}
vsyncTimeout = -1;
}
vsyncAsyncCallback = null;
cb.complete(null);
if(vsyncTimeout != -1) {
Window.clearTimeout(vsyncTimeout);
vsyncTimeout = -1;
}
cb.complete(null);
}
});
vsyncTimeout = timeout[0] = Window.setTimeout(() -> {
if(!hasTimedOut[0]) {
hasTimedOut[0] = true;
if(vsyncWaiting != -1l) {
if(vsyncTimeout != -1) {
vsyncTimeout = -1;
vsyncWaiting = -1l;
vsyncAsyncCallback = null;
cb.complete(null);
}
}
@ -1578,13 +1533,6 @@ public class PlatformInput {
Window.clearTimeout(mouseUngrabTimeout);
mouseUngrabTimeout = -1;
}
if(vsyncSaveLockInterval != -1) {
try {
Window.clearInterval(vsyncSaveLockInterval);
}catch(Throwable t) {
}
vsyncSaveLockInterval = -1;
}
if(touchKeyboardField != null) {
touchKeyboardField.blur();
if(parent != null) {

View File

@ -32,6 +32,7 @@ import org.teavm.jso.browser.Window;
import org.teavm.jso.core.JSArrayReader;
import org.teavm.jso.core.JSError;
import org.teavm.jso.dom.css.CSSStyleDeclaration;
import org.teavm.jso.dom.html.HTMLButtonElement;
import org.teavm.jso.dom.html.HTMLCanvasElement;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
@ -292,6 +293,46 @@ public class ClientMain {
}
}
private static HTMLElement createToolButtons(HTMLDocument doc) {
HTMLButtonElement buttonResetSettings = (HTMLButtonElement) doc.createElement("button");
buttonResetSettings.setAttribute("style", "margin-left:10px;");
buttonResetSettings.setInnerText("Reset Settings");
buttonResetSettings.addEventListener("click", (evt) -> {
boolean y = false;
if (Window.confirm("Do you want to reset client settings?")) {
PlatformApplication.setLocalStorage("g", null);
PlatformApplication.setLocalStorage("p", null);
y = true;
}
if (Window.confirm("Do you want to reset servers and relays?")) {
PlatformApplication.setLocalStorage("r", null);
PlatformApplication.setLocalStorage("s", null);
y = true;
}
if (y) {
Window.alert("Settings reset.");
}
});
HTMLButtonElement buttonOpenConsole = (HTMLButtonElement) doc.createElement("button");
buttonOpenConsole.setAttribute("style", "margin-left:10px;");
buttonOpenConsole.setInnerText("Open Debug Console");
buttonOpenConsole.addEventListener("click", (evt) -> {
DebugConsoleWindow.showDebugConsole();
});
HTMLElement div1 = doc.createElement("div");
div1.setAttribute("style", "position:absolute;bottom:5px;right:0px;");
div1.appendChild(buttonResetSettings);
div1.appendChild(buttonOpenConsole);
HTMLElement div2 = doc.createElement("div");
div2.setAttribute("style", "position:relative;");
div2.appendChild(div1);
HTMLElement div3 = doc.createElement("div");
div3.getClassList().add("_eaglercraftX_crash_tools_element");
div3.setAttribute("style", "z-index:101;position:absolute;top:135px;left:10%;right:10%;height:0px;");
div3.appendChild(div2);
return div3;
}
public static void showCrashScreen(String message, Throwable t) {
try {
showCrashScreen(message + "\n\n" + EagRuntime.getStackTrace(t));
@ -410,6 +451,7 @@ public class ClientMain {
div.getClassList().add("_eaglercraftX_crash_element");
el.appendChild(img);
el.appendChild(div);
el.appendChild(createToolButtons(doc));
div.appendChild(doc.createTextNode(strFinal));
PlatformRuntime.removeEventHandlers();
@ -581,6 +623,7 @@ public class ClientMain {
div.getClassList().add("_eaglercraftX_incompatible_element");
el.appendChild(img);
el.appendChild(div);
el.appendChild(createToolButtons(doc));
div.setInnerHTML("<h2><svg style=\"vertical-align:middle;margin:0px 16px 8px 8px;\" xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 48 48\" fill=\"none\"><path stroke=\"#000000\" stroke-width=\"3\" stroke-linecap=\"square\" d=\"M1.5 8.5v34h45v-28m-3-3h-10v-3m-3-3h-10m15 6h-18v-3m-3-3h-10\"/><path stroke=\"#000000\" stroke-width=\"2\" stroke-linecap=\"square\" d=\"M12 21h0m0 4h0m4 0h0m0-4h0m-2 2h0m20-2h0m0 4h0m4 0h0m0-4h0m-2 2h0\"/><path stroke=\"#000000\" stroke-width=\"2\" stroke-linecap=\"square\" d=\"M20 30h0 m2 2h0 m2 2h0 m2 2h0 m2 -2h0 m2 -2h0 m2 -2h0\"/></svg>+ This device is incompatible with Eaglercraft&ensp;:(</h2>"
+ "<div style=\"margin-left:40px;\">"
+ "<p style=\"font-size:1.2em;\"><b style=\"font-size:1.1em;\">Issue:</b> <span style=\"color:#BB0000;\" id=\"_eaglercraftX_crashReason\"></span><br /></p>"
@ -664,14 +707,17 @@ public class ClientMain {
div.getClassList().add("_eaglercraftX_context_lost_element");
el.appendChild(img);
el.appendChild(div);
el.appendChild(createToolButtons(doc));
div.setInnerHTML("<h2><svg style=\"vertical-align:middle;margin:0px 16px 8px 8px;\" xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 48 48\" fill=\"none\"><path stroke=\"#000000\" stroke-width=\"3\" stroke-linecap=\"square\" d=\"M1.5 8.5v34h45v-28m-3-3h-10v-3m-3-3h-10m15 6h-18v-3m-3-3h-10\"/><path stroke=\"#000000\" stroke-width=\"2\" stroke-linecap=\"square\" d=\"M12 21h0m0 4h0m4 0h0m0-4h0m-2 2h0m20-2h0m0 4h0m4 0h0m0-4h0m-2 2h0\"/><path stroke=\"#000000\" stroke-width=\"2\" stroke-linecap=\"square\" d=\"M20 30h0 m2 2h0 m2 2h0 m2 2h0 m2 -2h0 m2 -2h0 m2 -2h0\"/></svg> + WebGL context lost!</h2>"
+ "<div style=\"margin-left:40px;\">"
+ "<p style=\"font-size:1.2em;\">Your browser has forcibly released all of the resources "
+ "allocated by the game's 3D rendering context. EaglercraftX cannot continue, please refresh "
+ "the page to restart the game.</p>"
+ "the page to restart the game, sorry for the inconvenience.</p>"
+ "<p style=\"font-size:1.2em;\">This is not a bug, it is usually caused by the browser "
+ "deciding it no longer has sufficient resources to continue rendering this page. If it "
+ "happens again, try closing your other browser tabs and windows.</p>"
+ "<p style=\"font-size:1.2em;\">If you're playing with vsync disabled, try enabling vsync "
+ "to allow the browser to control the GPU usage more precisely.</p>"
+ "<p style=\"overflow-wrap:break-word;white-space:pre-wrap;font:0.75em monospace;margin-top:1.5em;\" id=\"_eaglercraftX_contextLostTrace\"></p>"
+ "</div>");

View File

@ -16,6 +16,7 @@
package net.lax1dude.eaglercraft.v1_8.internal.teavm;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
@ -27,13 +28,15 @@ import com.jcraft.jzlib.CRC32;
import com.jcraft.jzlib.GZIPInputStream;
import com.jcraft.jzlib.InflaterInputStream;
import net.lax1dude.eaglercraft.v1_8.IOUtils;
public class EPKLoader {
public static final void loadEPK(ArrayBuffer epkFile, Map<String, byte[]> loadedFiles) throws IOException {
public static void loadEPK(ArrayBuffer epkFile, Map<String, byte[]> loadedFiles) throws IOException {
loadEPK(epkFile, "", loadedFiles);
}
public static final void loadEPK(ArrayBuffer epkFile, String path, Map<String, byte[]> loadedFiles) throws IOException {
public static void loadEPK(ArrayBuffer epkFile, String path, Map<String, byte[]> loadedFiles) throws IOException {
int byteLength = epkFile.getByteLength();
int l = byteLength - 16;
if(l < 1) {
@ -43,7 +46,7 @@ public class EPKLoader {
ArrayBufferInputStream is = new ArrayBufferInputStream(epkFile, 0, byteLength - 8);
byte[] header = new byte[8];
is.read(header);
IOUtils.readFully(is, header);
String type = readASCII(header);
if(!"EAGPKG$$".equals(type)) {
@ -65,13 +68,13 @@ public class EPKLoader {
throw new IOException("Unknown or invalid EPK version: " + vers);
}
is.skip(is.read()); // skip filename
is.skip(loadShort(is)); // skip comment
is.skip(8); // skip millis date
IOUtils.skipFully(is, loadByte(is)); // skip filename
IOUtils.skipFully(is, loadShort(is)); // skip comment
IOUtils.skipFully(is, 8); // skip millis date
int numFiles = loadInt(is);
char compressionType = (char)is.read();
char compressionType = (char)loadByte(is);
InputStream zis;
switch(compressionType) {
@ -112,11 +115,11 @@ public class EPKLoader {
if(i == 0) {
if(blockType == blockHead) {
byte[] readType = new byte[len];
zis.read(readType);
IOUtils.readFully(zis, readType);
if(!"file-type".equals(name) || !"epk/resources".equals(readASCII(readType))) {
throw new IOException("EPK is not of file-type 'epk/resources'!");
}
if(zis.read() != '>') {
if(loadByte(zis) != '>') {
throw new IOException("Object '" + name + "' is incomplete");
}
continue;
@ -133,7 +136,7 @@ public class EPKLoader {
int expectedCRC = loadInt(zis);
byte[] load = new byte[len - 5];
zis.read(load);
IOUtils.readFully(zis, load);
if(len > 5) {
crc32.reset();
@ -143,16 +146,16 @@ public class EPKLoader {
}
}
if(zis.read() != ':') {
if(loadByte(zis) != ':') {
throw new IOException("File '" + name + "' is incomplete");
}
loadedFiles.put(path + name, load);
}else {
zis.skip(len);
IOUtils.skipFully(zis, len);
}
if(zis.read() != '>') {
if(loadByte(zis) != '>') {
throw new IOException("Object '" + name + "' is incomplete");
}
}
@ -163,28 +166,36 @@ public class EPKLoader {
zis.close();
}
private static final int loadShort(InputStream is) throws IOException {
return (is.read() << 8) | is.read();
private static int loadByte(InputStream is) throws IOException {
int i = is.read();
if (i < 0) {
throw new EOFException();
}
return i;
}
private static final int loadInt(InputStream is) throws IOException {
return (is.read() << 24) | (is.read() << 16) | (is.read() << 8) | is.read();
private static int loadShort(InputStream is) throws IOException {
return (loadByte(is) << 8) | loadByte(is);
}
private static final String readASCII(byte[] bytesIn) throws IOException {
private static int loadInt(InputStream is) throws IOException {
return (loadByte(is) << 24) | (loadByte(is) << 16) | (loadByte(is) << 8) | loadByte(is);
}
private static String readASCII(byte[] bytesIn) throws IOException {
char[] charIn = new char[bytesIn.length];
for(int i = 0; i < bytesIn.length; ++i) {
charIn[i] = (char)((int)bytesIn[i] & 0xFF);
}
return new String(charIn);
}
private static final String readASCII(InputStream bytesIn) throws IOException {
int len = bytesIn.read();
private static String readASCII(InputStream bytesIn) throws IOException {
int len = loadByte(bytesIn);
char[] charIn = new char[len];
for(int i = 0; i < len; ++i) {
charIn[i] = (char)(bytesIn.read() & 0xFF);
charIn[i] = (char)loadByte(bytesIn);
}
return new String(charIn);
}

View File

@ -67,9 +67,6 @@ public class PCMToWAVLoader {
if (val < -32768) {
val = -32768;
}
if (val < 0) {
val |= 32768;
}
bufferOut.putShort((short)val);
}
}

View File

@ -90,6 +90,8 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
private boolean ramdiskMode = false;
private boolean singleThreadMode = false;
private boolean enableEPKVersionCheck = true;
private boolean keepAliveHack = true;
private boolean finishOnSwap = true;
public void loadNative(JSObject jsObject) {
integratedServerOpts = new JSONObject();
@ -139,6 +141,8 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
ramdiskMode = eaglercraftXOpts.getRamdiskMode(false);
singleThreadMode = eaglercraftXOpts.getSingleThreadMode(false);
enableEPKVersionCheck = eaglercraftXOpts.getEnableEPKVersionCheck(true);
keepAliveHack = eaglercraftXOpts.getKeepAliveHack(true);
finishOnSwap = eaglercraftXOpts.getFinishOnSwap(true);
JSEaglercraftXOptsHooks hooksObj = eaglercraftXOpts.getHooks();
if(hooksObj != null) {
hooks.loadHooks(hooksObj);
@ -270,6 +274,8 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
ramdiskMode = eaglercraftOpts.optBoolean("ramdiskMode", false);
singleThreadMode = eaglercraftOpts.optBoolean("singleThreadMode", false);
enableEPKVersionCheck = eaglercraftOpts.optBoolean("enableEPKVersionCheck", true);
keepAliveHack = eaglercraftOpts.optBoolean("keepAliveHack", true);
finishOnSwap = eaglercraftOpts.optBoolean("finishOnSwap", true);
defaultServers.clear();
JSONArray serversArray = eaglercraftOpts.optJSONArray("servers");
if(serversArray != null) {
@ -556,6 +562,14 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
return false;
}
public boolean isKeepAliveHackTeaVM() {
return keepAliveHack;
}
public boolean isFinishOnSwapTeaVM() {
return finishOnSwap;
}
@Override
public IClientConfigAdapterHooks getHooks() {
return hooks;
@ -608,6 +622,8 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
jsonObject.put("ramdiskMode", ramdiskMode);
jsonObject.put("singleThreadMode", singleThreadMode);
jsonObject.put("enableEPKVersionCheck", enableEPKVersionCheck);
jsonObject.put("keepAliveHack", keepAliveHack);
jsonObject.put("finishOnSwap", finishOnSwap);
JSONArray serversArr = new JSONArray();
for(int i = 0, l = defaultServers.size(); i < l; ++i) {
DefaultServer srv = defaultServers.get(i);

View File

@ -149,10 +149,14 @@ public class TeaVMClientConfigAdapterHooks implements IClientConfigAdapterHooks
}
public void loadHooks(JSEaglercraftXOptsHooks hooks) {
saveHook = (LocalStorageSaveHook)hooks.getLocalStorageSavedHook();
loadHook = (LocalStorageLoadHook)hooks.getLocalStorageLoadedHook();
crashHook = (CrashReportHook)hooks.getCrashReportHook();
screenChangedHook = (ScreenChangeHook)hooks.getScreenChangedHook();
JSObject obj = hooks.getLocalStorageSavedHook();
saveHook = obj != null ? (LocalStorageSaveHook) obj : null;
obj = hooks.getLocalStorageLoadedHook();
loadHook = obj != null ? (LocalStorageLoadHook) obj : null;
obj = hooks.getCrashReportHook();
crashHook = obj != null ? (CrashReportHook) obj : null;
obj = hooks.getScreenChangedHook();
screenChangedHook = obj != null ? (ScreenChangeHook) obj : null;
}
}

View File

@ -178,4 +178,10 @@ public abstract class JSEaglercraftXOptsRoot implements JSObject {
@JSBody(params = { "def" }, script = "return (typeof this.enableEPKVersionCheck === \"boolean\") ? this.enableEPKVersionCheck : def;")
public native boolean getEnableEPKVersionCheck(boolean deobfStackTraces);
@JSBody(params = { "def" }, script = "return (typeof this.keepAliveHack === \"boolean\") ? this.keepAliveHack : def;")
public native boolean getKeepAliveHack(boolean keepAliveHack);
@JSBody(params = { "def" }, script = "return (typeof this.finishOnSwap === \"boolean\") ? this.finishOnSwap : def;")
public native boolean getFinishOnSwap(boolean finishOnSwap);
}