u39
This commit is contained in:
parent
9d1bc659e2
commit
df4c705f60
|
@ -1,4 +1,4 @@
|
||||||
@echo off
|
@echo off
|
||||||
title gradlew generateJavascript
|
title gradlew generateJavascript
|
||||||
gradlew generateJavascript
|
call gradlew generateJavascript
|
||||||
pause
|
pause
|
|
@ -196,6 +196,7 @@ The default eaglercraftXOpts values is this:
|
||||||
- `eaglerNoDelay:` can be used to disable "Vigg's Algorithm", an algorithm that delays and combines multiple EaglercraftX packets together if they are sent in the same tick (does not affect regular Minecraft 1.8 packets)
|
- `eaglerNoDelay:` can be used to disable "Vigg's Algorithm", an algorithm that delays and combines multiple EaglercraftX packets together if they are sent in the same tick (does not affect regular Minecraft 1.8 packets)
|
||||||
- `ramdiskMode:` if worlds and resource packs should be stored in RAM instead of IndexedDB
|
- `ramdiskMode:` if worlds and resource packs should be stored in RAM instead of IndexedDB
|
||||||
- `singleThreadMode:` if the game should run the client and integrated server in the same context instead of creating a worker object
|
- `singleThreadMode:` if the game should run the client and integrated server in the same context instead of creating a worker object
|
||||||
|
- `enableEPKVersionCheck:` if the game should attempt to bypass the browser's cache and retry downloading assets.epk when its outdated
|
||||||
- `hooks:` can be used to define JavaScript callbacks for certain events
|
- `hooks:` can be used to define JavaScript callbacks for certain events
|
||||||
* `localStorageSaved:` JavaScript callback to save local storage keys (key, data)
|
* `localStorageSaved:` JavaScript callback to save local storage keys (key, data)
|
||||||
* `localStorageLoaded:` JavaScript callback to load local storage keys (key) returns data
|
* `localStorageLoaded:` JavaScript callback to load local storage keys (key) returns data
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
u39
|
|
@ -62,5 +62,6 @@
|
||||||
"disableBlobURLs": false,
|
"disableBlobURLs": false,
|
||||||
"eaglerNoDelay": false,
|
"eaglerNoDelay": false,
|
||||||
"ramdiskMode": false,
|
"ramdiskMode": false,
|
||||||
"singleThreadMode": false
|
"singleThreadMode": false,
|
||||||
|
"enableEPKVersionCheck": true
|
||||||
}
|
}
|
|
@ -62,5 +62,6 @@
|
||||||
"disableBlobURLs": false,
|
"disableBlobURLs": false,
|
||||||
"eaglerNoDelay": false,
|
"eaglerNoDelay": false,
|
||||||
"ramdiskMode": false,
|
"ramdiskMode": false,
|
||||||
"singleThreadMode": false
|
"singleThreadMode": false,
|
||||||
|
"enableEPKVersionCheck": true
|
||||||
}
|
}
|
|
@ -62,5 +62,6 @@
|
||||||
"disableBlobURLs": false,
|
"disableBlobURLs": false,
|
||||||
"eaglerNoDelay": false,
|
"eaglerNoDelay": false,
|
||||||
"ramdiskMode": false,
|
"ramdiskMode": false,
|
||||||
"singleThreadMode": false
|
"singleThreadMode": false,
|
||||||
|
"enableEPKVersionCheck": true
|
||||||
}
|
}
|
|
@ -49,6 +49,14 @@ eaglercraft.editCape.clearCape=Clear List
|
||||||
|
|
||||||
eaglercraft.editProfile.importExport=Import/Export
|
eaglercraft.editProfile.importExport=Import/Export
|
||||||
|
|
||||||
|
eaglercraft.defaultUsernameDetected.title=Default Username Detected
|
||||||
|
eaglercraft.defaultUsernameDetected.text0=The username "%s" was auto-generated by Eaglercraft
|
||||||
|
eaglercraft.defaultUsernameDetected.text1=It may already be taken on the largest servers
|
||||||
|
eaglercraft.defaultUsernameDetected.text2=Would you like to pick a different username instead?
|
||||||
|
eaglercraft.defaultUsernameDetected.changeUsername=Change Username
|
||||||
|
eaglercraft.defaultUsernameDetected.continueAnyway=Continue Anyway
|
||||||
|
eaglercraft.defaultUsernameDetected.doNotShow=Do not show again
|
||||||
|
|
||||||
eaglercraft.settingsBackup.importExport.title=What do you wanna do?
|
eaglercraft.settingsBackup.importExport.title=What do you wanna do?
|
||||||
eaglercraft.settingsBackup.importExport.import=Import Profile and Settings...
|
eaglercraft.settingsBackup.importExport.import=Import Profile and Settings...
|
||||||
eaglercraft.settingsBackup.importExport.export=Export Profile and Settings...
|
eaglercraft.settingsBackup.importExport.export=Export Profile and Settings...
|
||||||
|
|
|
@ -1394,7 +1394,7 @@ public class Minecraft implements IThreadListener {
|
||||||
int j = Mouse.getEventDWheel();
|
int j = Mouse.getEventDWheel();
|
||||||
if (j != 0) {
|
if (j != 0) {
|
||||||
if (this.isZoomKey) {
|
if (this.isZoomKey) {
|
||||||
this.adjustedZoomValue = MathHelper.clamp_float(adjustedZoomValue - j * 4.0f, 5.0f,
|
this.adjustedZoomValue = MathHelper.clamp_float(adjustedZoomValue - j * 4.0f, 4.0f,
|
||||||
32.0f);
|
32.0f);
|
||||||
} else if (this.thePlayer.isSpectator()) {
|
} else if (this.thePlayer.isSpectator()) {
|
||||||
j = j < 0 ? -1 : 1;
|
j = j < 0 ? -1 : 1;
|
||||||
|
|
|
@ -226,6 +226,7 @@ public class GameSettings {
|
||||||
public boolean enableProfanityFilter = false;
|
public boolean enableProfanityFilter = false;
|
||||||
public boolean hasShownProfanityFilter = false;
|
public boolean hasShownProfanityFilter = false;
|
||||||
public float touchControlOpacity = 1.0f;
|
public float touchControlOpacity = 1.0f;
|
||||||
|
public boolean hideDefaultUsernameWarning = false;
|
||||||
|
|
||||||
public int voiceListenRadius = 16;
|
public int voiceListenRadius = 16;
|
||||||
public float voiceListenVolume = 0.5f;
|
public float voiceListenVolume = 0.5f;
|
||||||
|
@ -1168,6 +1169,10 @@ public class GameSettings {
|
||||||
touchControlOpacity = parseFloat(astring[1]);
|
touchControlOpacity = parseFloat(astring[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (astring[0].equals("hideDefaultUsernameWarning")) {
|
||||||
|
this.hideDefaultUsernameWarning = astring[1].equals("true");
|
||||||
|
}
|
||||||
|
|
||||||
deferredShaderConf.readOption(astring[0], astring[1]);
|
deferredShaderConf.readOption(astring[0], astring[1]);
|
||||||
} catch (Exception var8) {
|
} catch (Exception var8) {
|
||||||
logger.warn("Skipping bad option: " + s);
|
logger.warn("Skipping bad option: " + s);
|
||||||
|
@ -1315,6 +1320,7 @@ public class GameSettings {
|
||||||
printwriter.println("screenRecordGameVolume:" + this.screenRecordGameVolume);
|
printwriter.println("screenRecordGameVolume:" + this.screenRecordGameVolume);
|
||||||
printwriter.println("screenRecordMicVolume:" + this.screenRecordMicVolume);
|
printwriter.println("screenRecordMicVolume:" + this.screenRecordMicVolume);
|
||||||
printwriter.println("touchControlOpacity:" + this.touchControlOpacity);
|
printwriter.println("touchControlOpacity:" + this.touchControlOpacity);
|
||||||
|
printwriter.println("hideDefaultUsernameWarning:" + this.hideDefaultUsernameWarning);
|
||||||
|
|
||||||
for (KeyBinding keybinding : this.keyBindings) {
|
for (KeyBinding keybinding : this.keyBindings) {
|
||||||
printwriter.println("key_" + keybinding.getKeyDescription() + ":" + keybinding.getKeyCode());
|
printwriter.println("key_" + keybinding.getKeyDescription() + ":" + keybinding.getKeyCode());
|
||||||
|
|
|
@ -479,7 +479,11 @@ public class PlatformInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int mouseGetEventDWheel() {
|
public static int mouseGetEventDWheel() {
|
||||||
return (int)currentMouseEvent.wheel;
|
return fixWheel(currentMouseEvent.wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int fixWheel(float val) {
|
||||||
|
return (val > 0.0f ? 1 : (val < 0.0f ? -1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int mouseGetX() {
|
public static int mouseGetX() {
|
||||||
|
|
|
@ -61,7 +61,7 @@ public class ArrayUtils {
|
||||||
public static String hexString(byte[] bytesIn) {
|
public static String hexString(byte[] bytesIn) {
|
||||||
char[] ret = new char[bytesIn.length << 1];
|
char[] ret = new char[bytesIn.length << 1];
|
||||||
for(int i = 0; i < bytesIn.length; ++i) {
|
for(int i = 0; i < bytesIn.length; ++i) {
|
||||||
ret[i << 1] = hex.charAt((bytesIn[i] >> 4) & 15);
|
ret[i << 1] = hex.charAt((bytesIn[i] >>> 4) & 15);
|
||||||
ret[(i << 1) + 1] = hex.charAt(bytesIn[i] & 15);
|
ret[(i << 1) + 1] = hex.charAt(bytesIn[i] & 15);
|
||||||
}
|
}
|
||||||
return new String(ret);
|
return new String(ret);
|
||||||
|
|
|
@ -20,10 +20,14 @@ public class EaglercraftVersion {
|
||||||
public static final String projectOriginName = "EaglercraftX";
|
public static final String projectOriginName = "EaglercraftX";
|
||||||
public static final String projectOriginAuthor = "lax1dude";
|
public static final String projectOriginAuthor = "lax1dude";
|
||||||
public static final String projectOriginRevision = "1.8";
|
public static final String projectOriginRevision = "1.8";
|
||||||
public static final String projectOriginVersion = "u38";
|
public static final String projectOriginVersion = "u39";
|
||||||
|
|
||||||
public static final String projectOriginURL = "https://gitlab.com/lax1dude/eaglercraftx-1.8"; // rest in peace
|
public static final String projectOriginURL = "https://gitlab.com/lax1dude/eaglercraftx-1.8"; // rest in peace
|
||||||
|
|
||||||
|
// EPK Version Identifier
|
||||||
|
|
||||||
|
public static final String EPKVersionIdentifier = "u39"; // Set to null to disable EPK version check
|
||||||
|
|
||||||
// Updating configuration
|
// Updating configuration
|
||||||
|
|
||||||
public static final boolean enableUpdateService = true;
|
public static final boolean enableUpdateService = true;
|
||||||
|
|
|
@ -271,6 +271,7 @@ public class EaglerFolderResourcePack extends AbstractResourcePack {
|
||||||
|
|
||||||
public static void loadRemoteResourcePack(String url, String hash, Consumer<EaglerFolderResourcePack> cb, Consumer<Runnable> ast, Runnable loading) {
|
public static void loadRemoteResourcePack(String url, String hash, Consumer<EaglerFolderResourcePack> cb, Consumer<Runnable> ast, Runnable loading) {
|
||||||
if (!isSupported || !hash.matches("^[a-f0-9]{40}$")) {
|
if (!isSupported || !hash.matches("^[a-f0-9]{40}$")) {
|
||||||
|
logger.error("Invalid character in resource pack hash! (is it lowercase?)");
|
||||||
cb.accept(null);
|
cb.accept(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -292,8 +293,9 @@ public class EaglerFolderResourcePack extends AbstractResourcePack {
|
||||||
digest.update(arr, 0, arr.length);
|
digest.update(arr, 0, arr.length);
|
||||||
byte[] hashOut = new byte[20];
|
byte[] hashOut = new byte[20];
|
||||||
digest.doFinal(hashOut, 0);
|
digest.doFinal(hashOut, 0);
|
||||||
if(!hash.equals(ArrayUtils.hexString(hashOut))) {
|
String hashOutStr = ArrayUtils.hexString(hashOut);
|
||||||
logger.error("Downloaded resource pack hash does not equal expected resource pack hash!");
|
if(!hash.equals(hashOutStr)) {
|
||||||
|
logger.error("Downloaded resource pack hash does not equal expected resource pack hash! ({} != {})", hashOutStr, hash);
|
||||||
cb.accept(null);
|
cb.accept(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import net.lax1dude.eaglercraft.v1_8.EaglerInputStream;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream;
|
import net.lax1dude.eaglercraft.v1_8.EaglerOutputStream;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom;
|
import net.lax1dude.eaglercraft.v1_8.EaglercraftRandom;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID;
|
import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.HString;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.nbt.CompressedStreamTools;
|
import net.minecraft.nbt.CompressedStreamTools;
|
||||||
import net.minecraft.nbt.NBTTagCompound;
|
import net.minecraft.nbt.NBTTagCompound;
|
||||||
|
@ -493,10 +494,9 @@ public class EaglerProfile {
|
||||||
rand = new EaglercraftRandom();
|
rand = new EaglercraftRandom();
|
||||||
|
|
||||||
do {
|
do {
|
||||||
username = defaultNames[rand.nextInt(defaultNames.length)] + defaultNames[rand.nextInt(defaultNames.length)]
|
username = HString.format("%s%s%04d", defaultNames[rand.nextInt(defaultNames.length)], defaultNames[rand.nextInt(defaultNames.length)], rand.nextInt(10000));
|
||||||
+ (100 + rand.nextInt(900));
|
}while(username.length() > 16);
|
||||||
} while (username.length() > 16);
|
|
||||||
|
|
||||||
setName(username);
|
setName(username);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -506,6 +506,11 @@ public class EaglerProfile {
|
||||||
|
|
||||||
presetCapeId = 0;
|
presetCapeId = 0;
|
||||||
customCapeId = -1;
|
customCapeId = -1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDefaultUsername(String str) {
|
||||||
|
return str.toLowerCase().matches("^(yeeish|yee|yeer|yeeler|eagler|eagl|darver|darvler|vool|vigg|deev|yigg|yeeg){2}\\d{2,4}$");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package net.lax1dude.eaglercraft.v1_8.profile;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.GuiButton;
|
||||||
|
import net.minecraft.client.gui.GuiScreen;
|
||||||
|
import net.minecraft.client.resources.I18n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class GuiScreenDefaultUsernameNote extends GuiScreen {
|
||||||
|
|
||||||
|
private final GuiScreen back;
|
||||||
|
private final GuiScreen cont;
|
||||||
|
|
||||||
|
public GuiScreenDefaultUsernameNote(GuiScreen back, GuiScreen cont) {
|
||||||
|
this.back = back;
|
||||||
|
this.cont = cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initGui() {
|
||||||
|
this.buttonList.clear();
|
||||||
|
this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 6 + 112, I18n.format("defaultUsernameDetected.changeUsername")));
|
||||||
|
this.buttonList.add(new GuiButton(1, this.width / 2 - 100, this.height / 6 + 142, I18n.format("defaultUsernameDetected.continueAnyway")));
|
||||||
|
this.buttonList.add(new GuiButton(2, this.width / 2 - 100, this.height / 6 + 172, I18n.format("defaultUsernameDetected.doNotShow")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drawScreen(int par1, int par2, float par3) {
|
||||||
|
this.drawDefaultBackground();
|
||||||
|
this.drawCenteredString(fontRendererObj, I18n.format("defaultUsernameDetected.title"), this.width / 2, 70, 11184810);
|
||||||
|
this.drawCenteredString(fontRendererObj, I18n.format("defaultUsernameDetected.text0", EaglerProfile.getName()), this.width / 2, 90, 16777215);
|
||||||
|
this.drawCenteredString(fontRendererObj, I18n.format("defaultUsernameDetected.text1"), this.width / 2, 105, 16777215);
|
||||||
|
this.drawCenteredString(fontRendererObj, I18n.format("defaultUsernameDetected.text2"), this.width / 2, 120, 16777215);
|
||||||
|
super.drawScreen(par1, par2, par3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void actionPerformed(GuiButton parGuiButton) {
|
||||||
|
if(parGuiButton.id == 0) {
|
||||||
|
this.mc.displayGuiScreen(back);
|
||||||
|
}else if(parGuiButton.id == 1) {
|
||||||
|
this.mc.displayGuiScreen(cont);
|
||||||
|
}else if(parGuiButton.id == 2) {
|
||||||
|
this.mc.gameSettings.hideDefaultUsernameWarning = true;
|
||||||
|
this.mc.gameSettings.saveOptions();
|
||||||
|
this.mc.displayGuiScreen(cont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -60,12 +60,14 @@ public class BootMenuEntryPoint {
|
||||||
return ((flag & 1) != 0 || IBootMenuConfigAdapter.instance.isShowBootMenuOnLaunch()) && !getHasAlreadyBooted();
|
return ((flag & 1) != 0 || IBootMenuConfigAdapter.instance.isShowBootMenuOnLaunch()) && !getHasAlreadyBooted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasInit = false;
|
||||||
private static byte[] signatureData = null;
|
private static byte[] signatureData = null;
|
||||||
private static byte[] bundleData = null;
|
private static byte[] bundleData = null;
|
||||||
|
|
||||||
public static void launchMenu(Window parentWindow, HTMLElement parentElement) {
|
public static void launchMenu(Window parentWindow, HTMLElement parentElement) {
|
||||||
signatureData = PlatformUpdateSvc.getClientSignatureData();
|
signatureData = PlatformUpdateSvc.getClientSignatureData();
|
||||||
bundleData = PlatformUpdateSvc.getClientBundleData();
|
bundleData = PlatformUpdateSvc.getClientBundleData();
|
||||||
|
hasInit = true;
|
||||||
BootMenuMain.launchMenu(parentWindow, parentElement);
|
BootMenuMain.launchMenu(parentWindow, parentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +85,11 @@ public class BootMenuEntryPoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isSignedClient() {
|
public static boolean isSignedClient() {
|
||||||
|
if(!hasInit) {
|
||||||
|
signatureData = PlatformUpdateSvc.getClientSignatureData();
|
||||||
|
bundleData = PlatformUpdateSvc.getClientBundleData();
|
||||||
|
hasInit = true;
|
||||||
|
}
|
||||||
return signatureData != null && bundleData != null;
|
return signatureData != null && bundleData != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -597,7 +597,7 @@ public class PlatformInput {
|
||||||
public void handleEvent(WheelEvent evt) {
|
public void handleEvent(WheelEvent evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
double delta = evt.getDeltaY();
|
double delta = -evt.getDeltaY();
|
||||||
mouseDWheel += delta;
|
mouseDWheel += delta;
|
||||||
if(hasShownPressAnyKey) {
|
if(hasShownPressAnyKey) {
|
||||||
int eventX = (int)(getOffsetX(evt, touchOffsetXTeaVM) * windowDPI);
|
int eventX = (int)(getOffsetX(evt, touchOffsetXTeaVM) * windowDPI);
|
||||||
|
@ -1246,7 +1246,11 @@ public class PlatformInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int mouseGetEventDWheel() {
|
public static int mouseGetEventDWheel() {
|
||||||
return (currentEvent.type == EVENT_MOUSE_WHEEL) ? (currentEvent.wheel == 0.0f ? 0 : (currentEvent.wheel > 0.0f ? -1 : 1)) : 0;
|
return (currentEvent.type == EVENT_MOUSE_WHEEL) ? fixWheel(currentEvent.wheel) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int fixWheel(float val) {
|
||||||
|
return (val > 0.0f ? 1 : (val < 0.0f ? -1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int mouseGetX() {
|
public static int mouseGetX() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -13,6 +14,7 @@ import java.util.function.Consumer;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EagRuntime;
|
import net.lax1dude.eaglercraft.v1_8.EagRuntime;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EagUtils;
|
import net.lax1dude.eaglercraft.v1_8.EagUtils;
|
||||||
import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID;
|
import net.lax1dude.eaglercraft.v1_8.EaglercraftUUID;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.EaglercraftVersion;
|
||||||
import net.lax1dude.eaglercraft.v1_8.Filesystem;
|
import net.lax1dude.eaglercraft.v1_8.Filesystem;
|
||||||
import net.lax1dude.eaglercraft.v1_8.boot_menu.teavm.BootMenuEntryPoint;
|
import net.lax1dude.eaglercraft.v1_8.boot_menu.teavm.BootMenuEntryPoint;
|
||||||
import org.teavm.interop.Async;
|
import org.teavm.interop.Async;
|
||||||
|
@ -48,7 +50,6 @@ import net.lax1dude.eaglercraft.v1_8.internal.buffer.ByteBuffer;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.buffer.EaglerArrayBufferAllocator;
|
import net.lax1dude.eaglercraft.v1_8.internal.buffer.EaglerArrayBufferAllocator;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer;
|
import net.lax1dude.eaglercraft.v1_8.internal.buffer.FloatBuffer;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer;
|
import net.lax1dude.eaglercraft.v1_8.internal.buffer.IntBuffer;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EPKLoader;
|
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ES6ShimStatus;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ES6ShimStatus;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EarlyLoadScreen;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EarlyLoadScreen;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EnumES6ShimStatus;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EnumES6ShimStatus;
|
||||||
|
@ -58,8 +59,8 @@ import net.lax1dude.eaglercraft.v1_8.internal.teavm.ImmediateContinue;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.MessageChannel;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.MessageChannel;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMBlobURLManager;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMBlobURLManager;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ClientMain;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ClientMain;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ClientMain.EPKFileEntry;
|
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.DebugConsoleWindow;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.DebugConsoleWindow;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.EPKDownloadHelper;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMClientConfigAdapter;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMClientConfigAdapter;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMDataURLManager;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMDataURLManager;
|
||||||
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMEnterBootMenuException;
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.TeaVMEnterBootMenuException;
|
||||||
|
@ -433,29 +434,14 @@ public class PlatformRuntime {
|
||||||
|
|
||||||
EarlyLoadScreen.paintScreen(glesVer, PlatformOpenGL.checkVAOCapable(), allowBootMenu);
|
EarlyLoadScreen.paintScreen(glesVer, PlatformOpenGL.checkVAOCapable(), allowBootMenu);
|
||||||
|
|
||||||
EPKFileEntry[] epkFiles = ClientMain.configEPKFiles;
|
if(PlatformAssets.assets == null || !PlatformAssets.assets.isEmpty()) {
|
||||||
|
PlatformAssets.assets = new HashMap<>();
|
||||||
for (int i = 0; i < epkFiles.length; ++i) {
|
|
||||||
String url = epkFiles[i].url;
|
|
||||||
String logURL = url.startsWith("data:") ? "<data: " + url.length() + " chars>" : url;
|
|
||||||
|
|
||||||
logger.info("Downloading: {}", logURL);
|
|
||||||
|
|
||||||
ArrayBuffer epkFileData = downloadRemoteURI(url);
|
|
||||||
|
|
||||||
if (epkFileData == null) {
|
|
||||||
throw new RuntimeInitializationFailureException("Could not download EPK file \"" + url + "\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Decompressing: {}", logURL);
|
|
||||||
|
|
||||||
try {
|
|
||||||
EPKLoader.loadEPK(epkFileData, epkFiles[i].path, PlatformAssets.assets);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
throw new RuntimeInitializationFailureException("Could not extract EPK file \"" + url + "\"", t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EPKDownloadHelper.downloadEPKFilesOfVersion(ClientMain.configEPKFiles,
|
||||||
|
teavmCfg.isEnableEPKVersionCheckTeaVM() ? EaglercraftVersion.EPKVersionIdentifier : null,
|
||||||
|
PlatformAssets.assets);
|
||||||
|
|
||||||
logger.info("Loaded {} resources from EPKs", PlatformAssets.assets.size());
|
logger.info("Loaded {} resources from EPKs", PlatformAssets.assets.size());
|
||||||
|
|
||||||
if(allowBootMenu && BootMenuEntryPoint.checkShouldLaunchFlag(win)) {
|
if(allowBootMenu && BootMenuEntryPoint.checkShouldLaunchFlag(win)) {
|
||||||
|
@ -613,6 +599,10 @@ public class PlatformRuntime {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasFetchSupportTeaVM() {
|
||||||
|
return hasFetchSupport;
|
||||||
|
}
|
||||||
|
|
||||||
public static void downloadRemoteURIByteArray(String assetPackageURI, final Consumer<byte[]> cb) {
|
public static void downloadRemoteURIByteArray(String assetPackageURI, final Consumer<byte[]> cb) {
|
||||||
downloadRemoteURI(assetPackageURI, arr -> cb.accept(TeaVMUtils.wrapByteArrayBuffer(arr)));
|
downloadRemoteURI(assetPackageURI, arr -> cb.accept(TeaVMUtils.wrapByteArrayBuffer(arr)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
package net.lax1dude.eaglercraft.v1_8.internal.teavm;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.teavm.jso.browser.Window;
|
||||||
|
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||||
|
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.Base64;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.internal.PlatformApplication;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.internal.PlatformRuntime;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.internal.RuntimeInitializationFailureException;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.internal.teavm.ClientMain.EPKFileEntry;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.log4j.LogManager;
|
||||||
|
import net.lax1dude.eaglercraft.v1_8.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class EPKDownloadHelper {
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger("BrowserRuntime");
|
||||||
|
|
||||||
|
public static void downloadEPKFilesOfVersion(EPKFileEntry[] epkFiles, String expectedVersionIdentifier,
|
||||||
|
Map<String, byte[]> loadedFiles) {
|
||||||
|
byte[] bTrue = Base64.decodeBase64("true");
|
||||||
|
boolean oldEPKInvalidFlag = Arrays.equals(bTrue, PlatformApplication.getLocalStorage("epkInvalidFlag", false));
|
||||||
|
boolean epkInvalidFlag = oldEPKInvalidFlag;
|
||||||
|
attempt_loop: for(int a = 0; a < 3; ++a) {
|
||||||
|
if(a == 1 && !PlatformRuntime.hasFetchSupportTeaVM()) continue;
|
||||||
|
loadedFiles.clear();
|
||||||
|
boolean canBeInvalid = expectedVersionIdentifier != null;
|
||||||
|
for(int i = 0; i < epkFiles.length; ++i) {
|
||||||
|
boolean noCache = false;
|
||||||
|
String url = null;
|
||||||
|
switch(a) {
|
||||||
|
case 0:
|
||||||
|
url = epkFiles[i].url;
|
||||||
|
noCache = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
logger.warn("Failed to download one or more correct/valid files, attempting to bypass the browser's cache...");
|
||||||
|
url = epkFiles[i].url;
|
||||||
|
noCache = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
logger.warn("Failed to download one or more correct/valid files, attempting to bypass the server's cache...");
|
||||||
|
url = injectCacheInvalidationHack(epkFiles[i].url, expectedVersionIdentifier);
|
||||||
|
noCache = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boolean b = url.startsWith("data:");
|
||||||
|
boolean c = !b && !url.startsWith("blob:");
|
||||||
|
String toCheck = url.indexOf("://") != -1 ? url : PlatformRuntime.win.getLocation().getFullURL();
|
||||||
|
boolean canBeCorrupt = c && (a < 1 || toCheck.startsWith("http:") || toCheck.startsWith("https:"));
|
||||||
|
canBeInvalid &= c;
|
||||||
|
String logURL = b ? "<data: " + url.length() + " chars>" : url;
|
||||||
|
|
||||||
|
logger.info("Downloading: {}", logURL);
|
||||||
|
|
||||||
|
ArrayBuffer epkFileData = PlatformRuntime.downloadRemoteURI(url, !noCache);
|
||||||
|
|
||||||
|
if(epkFileData == null) {
|
||||||
|
if(a < 2 && canBeCorrupt) {
|
||||||
|
logger.error("Could not download EPK file \"{}\"", logURL);
|
||||||
|
continue attempt_loop;
|
||||||
|
}else {
|
||||||
|
throw new RuntimeInitializationFailureException("Could not download EPK file \"" + logURL + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Decompressing: {}", logURL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
EPKLoader.loadEPK(epkFileData, epkFiles[i].path, loadedFiles);
|
||||||
|
}catch(Throwable t) {
|
||||||
|
if(a < 2 && canBeCorrupt) {
|
||||||
|
logger.error("Could not extract EPK file \"{}\"", logURL);
|
||||||
|
continue attempt_loop;
|
||||||
|
}else {
|
||||||
|
throw new RuntimeInitializationFailureException("Could not extract EPK file \"" + logURL + "\"", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(canBeInvalid) {
|
||||||
|
byte[] dat = loadedFiles.get("EPKVersionIdentifier.txt");
|
||||||
|
if(dat != null) {
|
||||||
|
String epkStr = (new String(dat, StandardCharsets.UTF_8)).trim();
|
||||||
|
if(expectedVersionIdentifier.equals(epkStr)) {
|
||||||
|
epkInvalidFlag = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
logger.error("EPK version identifier \"{}\" does not match the expected identifier \"{}\"", epkStr, expectedVersionIdentifier);
|
||||||
|
}else {
|
||||||
|
logger.error("Version identifier file is missing from the EPK, expecting \"{}\"", expectedVersionIdentifier);
|
||||||
|
}
|
||||||
|
if(epkInvalidFlag) {
|
||||||
|
break;
|
||||||
|
}else {
|
||||||
|
if(a < 2) {
|
||||||
|
continue;
|
||||||
|
}else {
|
||||||
|
logger.error("Nothing we can do about this, ignoring the invalid EPK version and setting invalid flag to true");
|
||||||
|
epkInvalidFlag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
epkInvalidFlag = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(epkInvalidFlag != oldEPKInvalidFlag) {
|
||||||
|
PlatformApplication.setLocalStorage("epkInvalidFlag", epkInvalidFlag ? bTrue : null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String injectCacheInvalidationHack(String url, String cacheFixStr) {
|
||||||
|
if(cacheFixStr != null) {
|
||||||
|
cacheFixStr = Window.encodeURIComponent(cacheFixStr);
|
||||||
|
}else {
|
||||||
|
cacheFixStr = "t" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
String toCheck = url.indexOf("://") != -1 ? url : PlatformRuntime.win.getLocation().getFullURL();
|
||||||
|
if(toCheck.startsWith("http:") || toCheck.startsWith("https:")) {
|
||||||
|
int i = url.indexOf('?');
|
||||||
|
if(i == url.length() - 1) {
|
||||||
|
return url + "eaglerCacheFix=" + cacheFixStr;
|
||||||
|
}else if(i != -1) {
|
||||||
|
String s = url.substring(i + 1);
|
||||||
|
if(!s.startsWith("&") && !s.startsWith("#")) {
|
||||||
|
s = "&" + s;
|
||||||
|
}
|
||||||
|
return url.substring(0, i + 1) + "eaglerCacheFix=" + cacheFixStr + s;
|
||||||
|
}else {
|
||||||
|
i = url.indexOf('#');
|
||||||
|
if(i != -1) {
|
||||||
|
return url.substring(0, i) + "?eaglerCacheFix=" + cacheFixStr + url.substring(i);
|
||||||
|
}else {
|
||||||
|
return url + "?eaglerCacheFix=" + cacheFixStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
|
||||||
private boolean eaglerNoDelay = false;
|
private boolean eaglerNoDelay = false;
|
||||||
private boolean ramdiskMode = false;
|
private boolean ramdiskMode = false;
|
||||||
private boolean singleThreadMode = false;
|
private boolean singleThreadMode = false;
|
||||||
|
private boolean enableEPKVersionCheck = true;
|
||||||
|
|
||||||
public void loadNative(JSObject jsObject) {
|
public void loadNative(JSObject jsObject) {
|
||||||
integratedServerOpts = new JSONObject();
|
integratedServerOpts = new JSONObject();
|
||||||
|
@ -134,6 +135,7 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
|
||||||
eaglerNoDelay = eaglercraftXOpts.getEaglerNoDelay(false);
|
eaglerNoDelay = eaglercraftXOpts.getEaglerNoDelay(false);
|
||||||
ramdiskMode = eaglercraftXOpts.getRamdiskMode(false);
|
ramdiskMode = eaglercraftXOpts.getRamdiskMode(false);
|
||||||
singleThreadMode = eaglercraftXOpts.getSingleThreadMode(false);
|
singleThreadMode = eaglercraftXOpts.getSingleThreadMode(false);
|
||||||
|
enableEPKVersionCheck = eaglercraftXOpts.getEnableEPKVersionCheck(true);
|
||||||
JSEaglercraftXOptsHooks hooksObj = eaglercraftXOpts.getHooks();
|
JSEaglercraftXOptsHooks hooksObj = eaglercraftXOpts.getHooks();
|
||||||
if(hooksObj != null) {
|
if(hooksObj != null) {
|
||||||
hooks.loadHooks(hooksObj);
|
hooks.loadHooks(hooksObj);
|
||||||
|
@ -263,6 +265,7 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
|
||||||
eaglerNoDelay = eaglercraftOpts.optBoolean("eaglerNoDelay", false);
|
eaglerNoDelay = eaglercraftOpts.optBoolean("eaglerNoDelay", false);
|
||||||
ramdiskMode = eaglercraftOpts.optBoolean("ramdiskMode", false);
|
ramdiskMode = eaglercraftOpts.optBoolean("ramdiskMode", false);
|
||||||
singleThreadMode = eaglercraftOpts.optBoolean("singleThreadMode", false);
|
singleThreadMode = eaglercraftOpts.optBoolean("singleThreadMode", false);
|
||||||
|
enableEPKVersionCheck = eaglercraftOpts.optBoolean("enableEPKVersionCheck", true);
|
||||||
defaultServers.clear();
|
defaultServers.clear();
|
||||||
JSONArray serversArray = eaglercraftOpts.optJSONArray("servers");
|
JSONArray serversArray = eaglercraftOpts.optJSONArray("servers");
|
||||||
if(serversArray != null) {
|
if(serversArray != null) {
|
||||||
|
@ -505,6 +508,10 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
|
||||||
return singleThreadMode;
|
return singleThreadMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEnableEPKVersionCheckTeaVM() {
|
||||||
|
return enableEPKVersionCheck;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isShowBootMenuOnLaunch() {
|
public boolean isShowBootMenuOnLaunch() {
|
||||||
return showBootMenuOnLaunch;
|
return showBootMenuOnLaunch;
|
||||||
|
@ -585,6 +592,7 @@ public class TeaVMClientConfigAdapter implements IClientConfigAdapter, IBootMenu
|
||||||
jsonObject.put("eaglerNoDelay", eaglerNoDelay);
|
jsonObject.put("eaglerNoDelay", eaglerNoDelay);
|
||||||
jsonObject.put("ramdiskMode", ramdiskMode);
|
jsonObject.put("ramdiskMode", ramdiskMode);
|
||||||
jsonObject.put("singleThreadMode", singleThreadMode);
|
jsonObject.put("singleThreadMode", singleThreadMode);
|
||||||
|
jsonObject.put("enableEPKVersionCheck", enableEPKVersionCheck);
|
||||||
JSONArray serversArr = new JSONArray();
|
JSONArray serversArr = new JSONArray();
|
||||||
for(int i = 0, l = defaultServers.size(); i < l; ++i) {
|
for(int i = 0, l = defaultServers.size(); i < l; ++i) {
|
||||||
DefaultServer srv = defaultServers.get(i);
|
DefaultServer srv = defaultServers.get(i);
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class TeaVMFetchJS {
|
||||||
@JSBody(params = { }, script = "return (typeof fetch === \"function\");")
|
@JSBody(params = { }, script = "return (typeof fetch === \"function\");")
|
||||||
public static native boolean checkFetchSupport();
|
public static native boolean checkFetchSupport();
|
||||||
|
|
||||||
@JSBody(params = { "uri", "forceCache", "callback" }, script = "fetch(uri, { cache: forceCache, mode: \"no-cors\" })"
|
@JSBody(params = { "uri", "forceCache", "callback" }, script = "fetch(uri, { cache: forceCache, mode: \"cors\" })"
|
||||||
+ ".then(function(res) { return res.arrayBuffer(); }).then(function(arr) { callback(arr); })"
|
+ ".then(function(res) { return res.arrayBuffer(); }).then(function(arr) { callback(arr); })"
|
||||||
+ ".catch(function(err) { console.error(err); callback(null); });")
|
+ ".catch(function(err) { console.error(err); callback(null); });")
|
||||||
public static native void doFetchDownload(String uri, String forceCache, FetchHandler callback);
|
public static native void doFetchDownload(String uri, String forceCache, FetchHandler callback);
|
||||||
|
|
|
@ -171,4 +171,7 @@ public abstract class JSEaglercraftXOptsRoot implements JSObject {
|
||||||
@JSBody(params = { "def" }, script = "return (typeof this.singleThreadMode === \"boolean\") ? this.singleThreadMode : def;")
|
@JSBody(params = { "def" }, script = "return (typeof this.singleThreadMode === \"boolean\") ? this.singleThreadMode : def;")
|
||||||
public native boolean getSingleThreadMode(boolean deobfStackTraces);
|
public native boolean getSingleThreadMode(boolean deobfStackTraces);
|
||||||
|
|
||||||
|
@JSBody(params = { "def" }, script = "return (typeof this.enableEPKVersionCheck === \"boolean\") ? this.enableEPKVersionCheck : def;")
|
||||||
|
public native boolean getEnableEPKVersionCheck(boolean deobfStackTraces);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue