diff options
author | syeyoung <cyoung06@naver.com> | 2022-08-05 16:01:15 +0900 |
---|---|---|
committer | syeyoung <cyoung06@naver.com> | 2022-08-05 16:01:15 +0900 |
commit | 7d2e27445cd64d32cbab35c48be42fe7e4d2dea1 (patch) | |
tree | f04de6981f778cc75d3e281640974fe5bda76844 /loader/src | |
parent | 315925f917021c399a92f1b6230710492f3c39fb (diff) | |
download | Skyblock-Dungeons-Guide-7d2e27445cd64d32cbab35c48be42fe7e4d2dea1.tar.gz Skyblock-Dungeons-Guide-7d2e27445cd64d32cbab35c48be42fe7e4d2dea1.tar.bz2 Skyblock-Dungeons-Guide-7d2e27445cd64d32cbab35c48be42fe7e4d2dea1.zip |
- Sorry, but I haven't got any good name for this commit
- This commit has been on my computer for like 6 months untouched
Diffstat (limited to 'loader/src')
22 files changed, 1673 insertions, 0 deletions
diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DGInterface.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DGInterface.java new file mode 100755 index 00000000..c88a3cbf --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DGInterface.java @@ -0,0 +1,31 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher; + +import net.minecraft.client.resources.IResourceManager; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; + +import java.io.File; + +public interface DGInterface { + void init(File resourceDir); + void unload(); + void onResourceReload(IResourceManager a); +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java new file mode 100644 index 00000000..7252a9db --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/DungeonsGuideReloadListener.java @@ -0,0 +1,24 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher; + +public interface DungeonsGuideReloadListener { + public void unloadReference(); + public void onLoad(DGInterface dgInterface); +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java new file mode 100755 index 00000000..c279d812 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/Main.java @@ -0,0 +1,296 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher; + +import kr.syeyoung.dungeonsguide.launcher.exceptions.AuthServerException; +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import kr.syeyoung.dungeonsguide.launcher.branch.ModDownloader; +import kr.syeyoung.dungeonsguide.launcher.exceptions.NoSuitableLoaderFoundException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.PrivacyPolicyRequiredException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.TokenExpiredException; +import kr.syeyoung.dungeonsguide.launcher.gui.GuiLoadingError; +import kr.syeyoung.dungeonsguide.launcher.loader.IDGLoader; +import kr.syeyoung.dungeonsguide.launcher.loader.JarLoader; +import kr.syeyoung.dungeonsguide.launcher.loader.LocalLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiErrorScreen; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.IReloadableResourceManager; +import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.ProgressManager; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.EventPriority; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Mod(modid = Main.MOD_ID, version = Main.VERSION) +public class Main +{ + public static final String MOD_ID = "dungeons_guide_wrapper"; + public static final String VERSION = "1.0"; + public static final String DOMAIN = "http://testmachine:8080/panel/api"; + + private static Main main; + + private File configDir; + + private DGInterface dgInterface; + private Authenticator authenticator = new Authenticator(); + private ModDownloader modDownloader = new ModDownloader(authenticator); + + private List<DungeonsGuideReloadListener> listeners = new ArrayList<>(); + + public void addDGReloadListener(DungeonsGuideReloadListener dungeonsGuideReloadListener) { + listeners.add(Objects.requireNonNull(dungeonsGuideReloadListener)); + } + public void removeDGReloadListener(DungeonsGuideReloadListener dungeonsGuideReloadListener) { + listeners.remove(dungeonsGuideReloadListener); + } + + private IDGLoader currentLoader; + + private Throwable lastError; + private boolean isMcLoaded; + + + + + @EventHandler + public void initEvent(FMLInitializationEvent initializationEvent) + { + MinecraftForge.EVENT_BUS.register(this); + if (dgInterface != null) { + try { + dgInterface.init(configDir); + + for (DungeonsGuideReloadListener listener : listeners) { + listener.onLoad(dgInterface); + } + } catch (Exception e) { + e.printStackTrace(); + lastError = e; + tryOpenError(); + } + } + } + + public void unload() throws ReferenceLeakedException { + if (currentLoader != null && !currentLoader.isUnloadable()) { + throw new UnsupportedOperationException("Current version is not unloadable"); + } + dgInterface = null; + for (DungeonsGuideReloadListener listener : listeners) { + listener.unloadReference(); + } + if (currentLoader != null) { + currentLoader.unloadJar(); + } + currentLoader = null; + } + public void load(IDGLoader newLoader) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + if (dgInterface != null) throw new IllegalStateException("DG is loaded"); + newLoader.loadJar(authenticator); + dgInterface = newLoader.getInstance(); + currentLoader = newLoader; + + dgInterface.init(configDir); + + for (DungeonsGuideReloadListener listener : listeners) { + listener.onLoad(dgInterface); + } + } + private void partialLoad(IDGLoader newLoader) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + if (dgInterface != null) throw new IllegalStateException("DG is loaded"); + newLoader.loadJar(authenticator); + dgInterface = newLoader.getInstance(); + currentLoader = newLoader; + } + + public void reload(IDGLoader newLoader) { + try { + unload(); + load(newLoader); + } catch (Exception e) { + e.printStackTrace(); + lastError = e; + dgInterface = null; + currentLoader = null; + tryOpenError(); + } + } + + public void tryOpenError() { + if (isMcLoaded) Minecraft.getMinecraft().displayGuiScreen(obtainErrorGUI()); + } + + public GuiScreen obtainErrorGUI() { + if (lastError instanceof PrivacyPolicyRequiredException) { + + } else if (lastError instanceof TokenExpiredException) { + + } else if (lastError instanceof NoSuitableLoaderFoundException) { + + } else if (lastError instanceof ReferenceLeakedException) { + + } else if (lastError instanceof AuthServerException) { + + } else if (lastError != null){ + return new GuiLoadingError(lastError, () -> {lastError = null;}); + } + if (lastError != null) + lastError.printStackTrace(); + // when gets called init and stuff remove thing + return null; + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onGuiOpen(GuiOpenEvent guiOpenEvent) { + if (guiOpenEvent.gui instanceof GuiMainMenu) { + isMcLoaded = true; + } + if (lastError != null && guiOpenEvent.gui instanceof GuiMainMenu) { + guiOpenEvent.gui = obtainErrorGUI(); + } + } + + public String getLoaderName(Configuration configuration) { + String loader = System.getProperty("dg.loader"); + if (loader == null) { + loader = configuration.get("loader", "modsource", "auto").getString(); + } + if (loader == null) loader = "auto"; + return loader; + } + + + public IDGLoader obtainLoader(Configuration configuration) { + String loader = getLoaderName(configuration); + + if ("local".equals(loader) || + (loader.equals("auto") && this.getClass().getResourceAsStream("/kr/syeyoung/dungeonsguide/DungeonsGuide.class") == null)) { + return new LocalLoader(); + } else if ("jar".equals(loader) || + (loader.equals("auto") && this.getClass().getResourceAsStream("/mod.jar") == null)) { + return new JarLoader(); + } else if (loader.equals("auto") ){ + // remote load + throw new UnsupportedOperationException(""); // yet + } else { + throw new NoSuitableLoaderFoundException(System.getProperty("dg.loader"), configuration.get("loader", "modsource", "auto").getString()); + } + } + + @EventHandler + public void preInit(FMLPreInitializationEvent preInitializationEvent) { + main = this; + configDir = preInitializationEvent.getModConfigurationDirectory(); + ProgressManager.ProgressBar bar = null; + try { + bar = ProgressManager.push("DungeonsGuide",2); + bar.step("Authenticating..."); + authenticator.repeatAuthenticate(5); + + File f = new File(preInitializationEvent.getModConfigurationDirectory(), "loader.cfg"); + Configuration configuration = new Configuration(f); + bar.step("Instantiating..."); + + partialLoad(obtainLoader(configuration)); + + configuration.save(); + } catch (Throwable t) { + t.printStackTrace(); + lastError = t; + dgInterface = null; + currentLoader = null; + tryOpenError(); + } finally { + if (bar != null) { + while(bar.getStep() < bar.getSteps()) bar.step(""); + ProgressManager.pop(bar); + } + } + + ((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(a -> { + if (dgInterface != null) dgInterface.onResourceReload(a); + }); +// try { +// token = authenticator.authenticateAndDownload(this.getClass().getResourceAsStream("/kr/syeyoung/dungeonsguide/DungeonsGuide.class") == null ? System.getProperty("dg.version") == null ? "nlatest" : System.getProperty("dg.version") : null); +// if (token != null) { +// main = this; +// URL.setURLStreamHandlerFactory(new DGStreamHandlerFactory(authenticator)); +// LaunchClassLoader classLoader = (LaunchClassLoader) Main.class.getClassLoader(); +// classLoader.addURL(new URL("z:///")); +// +// try { +// progressBar.step("Initializing"); +// this.dgInterface = new DungeonsGuide(authenticator); +// this.dgInterface.pre(preInitializationEvent); +// while (progressBar.getStep() < progressBar.getSteps()) +// progressBar.step("random-"+progressBar.getStep()); +// ProgressManager.pop(progressBar); +// isLoaded = true; +// } catch (Throwable e) { +// cause = e; +// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +// PrintStream printStream = new PrintStream(byteArrayOutputStream); +// e.printStackTrace(printStream); +// stacktrace = new String(byteArrayOutputStream.toByteArray()); +// +// while (progressBar.getStep() < progressBar.getSteps()) +// progressBar.step("random-"+progressBar.getStep()); +// ProgressManager.pop(progressBar); +// +// e.printStackTrace(); +// } +// } +// } catch (IOException | AuthenticationException | NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException | InvalidKeySpecException | SignatureException e) { +// cause = e; +// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +// PrintStream printStream = new PrintStream(byteArrayOutputStream); +// e.printStackTrace(printStream); +// stacktrace = new String(byteArrayOutputStream.toByteArray()); +// +// while (progressBar.getStep() < progressBar.getSteps()) +// progressBar.step("random-"+progressBar.getStep()); +// ProgressManager.pop(progressBar); +// +// e.printStackTrace(); +// } + } + + public void setLastError(Throwable t) { + lastError = t; + tryOpenError(); + } + + public static Main getMain() { + return main; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/Authenticator.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/Authenticator.java new file mode 100755 index 00000000..627a058f --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/Authenticator.java @@ -0,0 +1,252 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.authentication; + +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import kr.syeyoung.dungeonsguide.launcher.Main; +import kr.syeyoung.dungeonsguide.launcher.exceptions.AuthServerException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.PrivacyPolicyRequiredException; +import kr.syeyoung.dungeonsguide.launcher.exceptions.TokenExpiredException; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Session; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.json.JSONObject; +import sun.reflect.Reflection; + +import javax.crypto.*; +import javax.net.ssl.*; +import java.io.*; +import java.math.BigInteger; +import java.net.*; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class Authenticator { + private String token; + private Instant validThru; + @Getter + private TokenStatus tokenStatus = TokenStatus.UNAUTHENTICATED; + + private final SecureRandom secureRandom = new SecureRandom(); + + private Lock authenticationLock = new ReentrantLock(); + + static { + Reflection.registerFieldsToFilter(Authenticator.class, "token"); // Please do not touch this field. I know there is a way to block it completely, but I won't do it here. + } + + public String getRawToken() { + return token; + } + public String getUnexpiredToken() { + if (tokenStatus != TokenStatus.AUTHENTICATED) throw new IllegalStateException("Token is not available"); + long expiry = getJwtPayload(token).getLong("exp"); + if (System.currentTimeMillis() >= expiry-2000 || tokenStatus == TokenStatus.EXPIRED) { + tokenStatus = TokenStatus.EXPIRED; + try { + repeatAuthenticate(5); + } catch (Throwable t) { + Main.getMain().setLastError(t); + throw new TokenExpiredException(t); + } + } + return token; + } + + + private byte[] generateSharedSecret() { + byte[] bts = new byte[32]; + secureRandom.nextBytes(bts); + return bts; + } + + public String repeatAuthenticate(int tries) { + int cnt = 0; + while(true) { + try { + reauthenticate(); + break; + } catch (IOException | AuthenticationException | NoSuchAlgorithmException e) { + e.printStackTrace(); + if (cnt == tries) throw new RuntimeException(e); + try { + Thread.sleep((long) Math.max(Math.pow(2, tries)* 100, 1000 * 10)); + } catch (InterruptedException ex) {} + } + cnt++; + } + return token; + } + public String reauthenticate() throws IOException, AuthenticationException, NoSuchAlgorithmException { + try { + authenticationLock.lock(); + + MinecraftSessionService yggdrasilMinecraftSessionService = Minecraft.getMinecraft().getSessionService(); + Session session = Minecraft.getMinecraft().getSession(); + + tokenStatus = TokenStatus.UNAUTHENTICATED; + token = null; + token = requestAuth(session.getProfile().getId(), session.getProfile().getName()); + JSONObject d = getJwtPayload(token); + + byte[] sharedSecret = generateSharedSecret(); + + + String hash = calculateServerHash(sharedSecret, + Base64.decodeBase64(d.getString("publicKey"))); + + byte[] encodedSharedSecret; + try { + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(d.getString("publicKey"))))); + encodedSharedSecret = cipher.doFinal(sharedSecret); + } catch (NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | + InvalidKeySpecException | + InvalidKeyException e) { + throw new RuntimeException(e); + } + + yggdrasilMinecraftSessionService.joinServer(session.getProfile(), session.getToken(), hash); // Sent to "MOJANG" Server. + JSONObject furtherStuff = verifyAuth(token, encodedSharedSecret); + token = furtherStuff.getString("jwt"); + if ("TOS_PRIVACY_POLICY_ACCEPT_REQUIRED".equals(furtherStuff.getString("result"))) { + tokenStatus = TokenStatus.PP_REQUIRED; + throw new PrivacyPolicyRequiredException(); + } + tokenStatus = TokenStatus.AUTHENTICATED; + return this.token; + } finally { + authenticationLock.unlock(); + } + } + + public String acceptLatestTOS() throws IOException { + try { + authenticationLock.lock(); + if (tokenStatus != TokenStatus.PP_REQUIRED) throw new IllegalStateException("Already accepted TOS"); + JSONObject furtherStuff = acceptPrivacyPolicy(token); + token = furtherStuff.getString("jwt"); + if ("TOS_PRIVACY_POLICY_ACCEPT_REQUIRED".equals(furtherStuff.getString("result"))) { + tokenStatus = TokenStatus.PP_REQUIRED; + throw new PrivacyPolicyRequiredException(); + } + tokenStatus = TokenStatus.AUTHENTICATED; + return this.token; + } finally { + authenticationLock.unlock(); + } + } + + public JSONObject getJwtPayload(String jwt) { + String midPart = jwt.split("\\.")[1].replace("+", "-").replace("/", "_"); + String base64Decode = new String(Base64.decodeBase64(midPart)); // padding + return new JSONObject(base64Decode); + } + + private String requestAuth(UUID uuid, String nickname) throws IOException { + HttpURLConnection urlConnection = request("POST", "/auth/v2/requestAuth"); + urlConnection.setRequestProperty("Content-Type", "application/json"); + + urlConnection.getOutputStream().write(("{\"uuid\":\""+uuid.toString()+"\",\"nickname\":\""+nickname+"\"}").getBytes()); + try (InputStream is = obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + if (urlConnection.getResponseCode() != 200) + System.out.println("/auth/requestAuth :: Received " + urlConnection.getResponseCode() + " along with\n" + payload); + + JSONObject json = new JSONObject(payload); + + if ("Success".equals(json.getString("status"))) { + return json.getString("data"); + } else { + throw new AuthServerException(json); + } + } + } + private JSONObject verifyAuth(String tempToken, byte[] secret) throws IOException { + HttpURLConnection urlConnection = request("POST", "/auth/v2/authenticate"); + + urlConnection.getOutputStream().write(("{\"jwt\":\""+tempToken+"\",\"sharedSecret\":\""+Base64.encodeBase64URLSafeString(secret)+"}").getBytes()); + try (InputStream is = obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + if (urlConnection.getResponseCode() != 200) + System.out.println("/auth/authenticate :: Received " + urlConnection.getResponseCode() + " along with\n" + payload); + + JSONObject jsonObject = new JSONObject(payload); + if (!"Success".equals(jsonObject.getString("status"))) { + throw new AuthServerException(jsonObject); + } + return jsonObject.getJSONObject("data"); + } + } + private JSONObject acceptPrivacyPolicy(String tempToken) throws IOException { + HttpURLConnection urlConnection = request("POST", "/auth/v2/acceptPrivacyPolicy"); + + urlConnection.getOutputStream().write(tempToken.getBytes()); + try (InputStream is = obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + if (urlConnection.getResponseCode() != 200) + System.out.println("/auth/authenticate :: Received " + urlConnection.getResponseCode() + " along with\n" + payload); + + JSONObject jsonObject = new JSONObject(payload); + if (!"Success".equals(jsonObject.getString("status"))) { + throw new AuthServerException(jsonObject); + } + return jsonObject.getJSONObject("data"); + } + } + + + private String calculateServerHash(byte[] a, byte[] b) throws NoSuchAlgorithmException { + MessageDigest c = MessageDigest.getInstance("SHA-1"); + c.update("".getBytes()); + c.update(a); + c.update(b); + byte[] d = c.digest(); + return new BigInteger(d).toString(16); + } + + public InputStream obtainInputStream(HttpURLConnection huc) { + InputStream inputStream = null; + try { + inputStream = huc.getInputStream(); + } catch (Exception e) { + inputStream = huc.getErrorStream(); + } + return inputStream; + } + public HttpURLConnection request(String method, String url) throws IOException { + HttpURLConnection urlConnection = (HttpURLConnection) new URL(Main.DOMAIN+url).openConnection(); + urlConnection.setRequestMethod(method); + urlConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + urlConnection.setAllowUserInteraction(true); + if (tokenStatus == TokenStatus.AUTHENTICATED) + urlConnection.setRequestProperty("Authorization", "Bearer "+getUnexpiredToken()); + return urlConnection; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/TokenStatus.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/TokenStatus.java new file mode 100644 index 00000000..a83818b8 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/authentication/TokenStatus.java @@ -0,0 +1,27 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.authentication; + +public enum TokenStatus { + UNAUTHENTICATED, + BANNED, + PP_REQUIRED, + AUTHENTICATED, + EXPIRED +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/ModDownloader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/ModDownloader.java new file mode 100644 index 00000000..45eacee5 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/ModDownloader.java @@ -0,0 +1,302 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.branch; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import lombok.Getter; +import net.minecraftforge.fml.common.ProgressManager; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ModDownloader { + private Authenticator authenticator; + + @Getter + private List<UpdateBranch> accessibleBranches = null; + + public ModDownloader(Authenticator authenticator) { + this.authenticator = authenticator; + } + + public List<UpdateBranch> fetchAccessibleBranches() throws IOException { + HttpURLConnection urlConnection = authenticator.request("GET", "/updates/"); + try (InputStream is = authenticator.obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + + JSONArray jsonArray = new JSONArray(payload); + List<UpdateBranch> branches = new ArrayList<>(); + for (Object o : jsonArray) { + if (o instanceof JSONObject) { + JSONObject branch = (JSONObject) o; + UpdateBranch updateBranch = new UpdateBranch(); + updateBranch.setId(branch.getLong("id")); + updateBranch.setName(branch.getString("name")); + updateBranch.setMetadata(branch.getJSONObject("metadata").getJSONObject("metadataSchema")); + branches.add(updateBranch); + } + } + return this.accessibleBranches = branches; + } + } + + public List<Update> fetchUpdates(Long branch, int page) throws IOException { + HttpURLConnection urlConnection = authenticator.request("GET", "/updates/"+branch+"/"); + try (InputStream is = authenticator.obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + + JSONArray jsonArray = new JSONArray(payload); + List<Update> updates = new ArrayList<>(); + for (Object o : jsonArray) { + if (o instanceof JSONObject) { + JSONObject json = (JSONObject) o; + Update update = new Update(); + update.setId(json.getLong("id")); + update.setBranchId(json.getLong("branchId")); + update.setName(json.getString("versionName")); + update.setUpdateLog(json.getString("updateLog")); + update.setMetadata(json.getJSONObject("metadata")); + for (Object assets : json.getJSONObject("assets").getJSONArray("assets")) { + if (assets instanceof JSONObject) { + JSONObject a_json = (JSONObject) assets; + Update.Asset asset = new Update.Asset(); + asset.setName(a_json.getString("name")); + asset.setSize(a_json.getLong("size")); + asset.setObjectId(a_json.getString("objectId")); + asset.setAssetId(UUID.fromString(a_json.getString("assetID"))); + update.getAssets().add(asset); + } + } + updates.add(update); + } + } + return updates; + } + } + + /* + pls Close after done + */ + public InputStream fetchAsset(Update update, UUID assetId) throws IOException { + HttpURLConnection urlConnection = authenticator.request("GET", "/updates/"+update.getBranchId()+"/"+update.getId()+"/"+assetId.toString()); + try (InputStream is = authenticator.obtainInputStream(urlConnection)) { + String payload = String.join("\n", IOUtils.readLines(is)); + JSONObject object = new JSONObject(payload); + + HttpURLConnection connection = (HttpURLConnection) new URL(object.getString("url")).openConnection(); + connection.setRequestMethod(object.getString("method")); + urlConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(true); + urlConnection.setAllowUserInteraction(true); + return authenticator.obtainInputStream(connection); + } + } + + + + + + + private ProgressManager.ProgressBar progressBar; + + private PublicKey dgPublicKey; + private PublicKey getDGPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + if (dgPublicKey != null) return dgPublicKey; + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxO89qtwG67jNucQ9Y44c" + + "IUs/B+5BeJPs7G+RG2gfs4/2+tzF/c1FLDc33M7yKw8aKk99vsBUY9Oo8gxxiEPB" + + "JitP/qfon2THp94oM77ZTpHlmFoqbZMcKGZVI8yfvEL4laTM8Hw+qh5poQwtpEbK" + + "Xo47AkxygxJasUnykER2+aSTZ6kWU2D4xiNtFA6lzqN+/oA+NaYfPS0amAvyVlHR" + + "n/8IuGkxb5RrlqVssQstFnxsJuv88qdGSEqlcKq2tLeg9hb8eCnl2OFzvXmgbVER" + + "0JaV+4Z02fVG1IlR3Xo1mSit7yIU6++3usRCjx2yfXpnGGJUW5pe6YETjNew3ax+" + + "FAZ4GePWCdmS7FvBnbbABKo5pE06ZTfDUTCjQlAJQiUgoF6ntMJvQAXPu48Vr8q/" + + "mTcuZWVnI6CDgyE7nNq3WNoq3397sBzxRohMxuqzl3T19zkfPKF05iV2Ju1HQMW5" + + "I119bYrmVD240aGESZc20Sx/9g1BFpNzQbM5PGUlWJ0dhLjl2ge4ip2hHciY3OEY" + + "p2Qy2k+xEdenpKdL+WMRimCQoO9gWe2Tp4NmP5dppDXZgPjXqjZpnGs0Uxs+fXqW" + + "cwlg3MbX3rFl9so/fhVf4p9oXZK3ve7z5D6XSSDRYECvsKIa08WAxJ/U6n204E/4" + + "xUF+3ZgFPdzZGn2PU7SsnOsCAwEAAQ==")); + return dgPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec); + } + + + + + private KeyPair rsaKey; + private KeyPair getKeyPair() { + KeyPairGenerator a = null; + try { + a = KeyPairGenerator.getInstance("RSA"); + } catch (NoSuchAlgorithmException b) { } + a.initialize(1024); + this.rsaKey = a.generateKeyPair(); + return this.rsaKey; + } + + + private final HashMap<String, byte[]> loadedResources = new HashMap<String, byte[]>(); + + public HashMap<String, byte[]> getResources() { + return loadedResources; + } + + private void downloadSafe(String dgToken, String url, boolean isValidateSignature) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, KeyManagementException, SignatureException, InvalidKeySpecException { + HttpsURLConnection dgConnection = (HttpsURLConnection) new URL(url).openConnection(); + dgConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + dgConnection.setRequestProperty("Content-Type", "application/json"); + dgConnection.setRequestMethod("GET"); + dgConnection.setRequestProperty("Authorization", dgToken); + dgConnection.setDoInput(true); + dgConnection.setDoOutput(true); + + InputStream inputStream = dgConnection.getInputStream(); + byte[] lengthBytes = new byte[4]; + inputStream.read(lengthBytes); + int length = ((lengthBytes[0] & 0xFF) << 24) | + ((lengthBytes[1] & 0xFF) << 16) | + ((lengthBytes[2] & 0xFF) << 8) | + ((lengthBytes[3] & 0xFF)); + while (inputStream.available() < length) ; + byte[] keyPayload = new byte[length]; + inputStream.read(keyPayload); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.rsaKey.getPrivate()); + byte[] h = cipher.doFinal(keyPayload); + + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec keySpec = new SecretKeySpec(h, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(h); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); + + cipherInputStream.read(lengthBytes); + length = ((lengthBytes[0] & 0xFF) << 24) | + ((lengthBytes[1] & 0xFF) << 16) | + ((lengthBytes[2] & 0xFF) << 8) | + ((lengthBytes[3] & 0xFF)); + + int totalLen = length; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buff = new byte[256]; + while (totalLen > 0) { + int len = cipherInputStream.read(buff, 0, Math.min(buff.length, totalLen)); + totalLen -= len; + bos.write(buff, 0, len); + } + byte[] body = bos.toByteArray(); + + byte[] signed = null; + if (isValidateSignature) { + progressBar.step("Validating Signature"); + cipherInputStream.read(lengthBytes,0 , 4); + length = ((lengthBytes[0] & 0xFF) << 24) | + ((lengthBytes[1] & 0xFF) << 16) | + ((lengthBytes[2] & 0xFF) << 8) | + ((lengthBytes[3] & 0xFF)); + + totalLen = length; + bos = new ByteArrayOutputStream(); + while (totalLen > 0) { + int len = cipherInputStream.read(buff, 0, Math.min(buff.length, totalLen)); + totalLen -= len; + bos.write(buff, 0, len); + } + signed = bos.toByteArray(); + + Signature sign = Signature.getInstance("SHA512withRSA"); + sign.initVerify(getDGPublicKey()); + sign.update(body); + boolean truth = sign.verify(signed); + if (!truth) throw new SignatureException("DG SIGNATURE FORGED"); + } + + ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(body)); + ZipEntry zipEntry; + while ((zipEntry=zipInputStream.getNextEntry()) != null) { + byte[] buffer = new byte[256]; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + int p = 0; + while((p = zipInputStream.read(buffer)) > 0) { + byteArrayOutputStream.write(buffer, 0, p); + } + this.loadedResources.put(zipEntry.getName(), byteArrayOutputStream.toByteArray()); + } + dgConnection.disconnect(); + } + + public JsonElement getJsonSecured(String u) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, KeyManagementException { + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL(u).openConnection(); + httpsURLConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); + httpsURLConnection.setRequestProperty("Content-Type", "application/json"); + httpsURLConnection.setRequestMethod("GET"); + httpsURLConnection.setRequestProperty("Authorization", authenticator.getUnexpiredToken()); + httpsURLConnection.setDoInput(true); + httpsURLConnection.setDoOutput(true); + + InputStream inputStream = httpsURLConnection.getInputStream(); + byte[] lengthPayload = new byte[4]; + inputStream.read(lengthPayload); + int length = ((lengthPayload[0] & 0xFF) << 24) | + ((lengthPayload[1] & 0xFF) << 16) | + ((lengthPayload[2] & 0xFF) << 8) | + ((lengthPayload[3] & 0xFF)); + while (inputStream.available() < length) ; + byte[] keyPayload = new byte[length]; + inputStream.read(keyPayload); + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.rsaKey.getPrivate()); + byte[] AESKey = cipher.doFinal(keyPayload); + + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + SecretKeySpec secretKeySpec = new SecretKeySpec(AESKey, "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(AESKey); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); + cipherInputStream.read(lengthPayload); + length = ((lengthPayload[0] & 0xFF) << 24) | + ((lengthPayload[1] & 0xFF) << 16) | + ((lengthPayload[2] & 0xFF) << 8) | + ((lengthPayload[3] & 0xFF)); + JsonElement l = new JsonParser().parse(new InputStreamReader(cipherInputStream)); + httpsURLConnection.disconnect(); + return l; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/Update.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/Update.java new file mode 100644 index 00000000..565ac5aa --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/Update.java @@ -0,0 +1,46 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.branch; + +import lombok.Data; +import org.json.JSONObject; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Data +public class Update { + private long id; + private long branchId; + private String name; + private String updateLog; + private JSONObject metadata; + private Instant releaseDate; + private List<Asset> assets = new ArrayList<>(); + + @Data + public static class Asset { + private String name; + private long size; + private String objectId; + private UUID assetId; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateBranch.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateBranch.java new file mode 100644 index 00000000..f0b05ee4 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/branch/UpdateBranch.java @@ -0,0 +1,29 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.branch; + +import lombok.Data; +import org.json.JSONObject; + +@Data +public class UpdateBranch { + private Long id; + private String name; + private JSONObject metadata; +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AuthServerException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AuthServerException.java new file mode 100644 index 00000000..22b851a8 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/AuthServerException.java @@ -0,0 +1,27 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +import lombok.AllArgsConstructor; +import org.json.JSONObject; + +@AllArgsConstructor +public class AuthServerException extends RuntimeException { + private JSONObject payload; +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoSuitableLoaderFoundException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoSuitableLoaderFoundException.java new file mode 100644 index 00000000..e5f50754 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/NoSuitableLoaderFoundException.java @@ -0,0 +1,27 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NoSuitableLoaderFoundException extends RuntimeException { + private String jvmFlag; + private String configuration; +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/PrivacyPolicyRequiredException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/PrivacyPolicyRequiredException.java new file mode 100644 index 00000000..006e03ca --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/PrivacyPolicyRequiredException.java @@ -0,0 +1,22 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +public class PrivacyPolicyRequiredException extends RuntimeException{ +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/ReferenceLeakedException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/ReferenceLeakedException.java new file mode 100644 index 00000000..3a88b862 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/ReferenceLeakedException.java @@ -0,0 +1,22 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +public class ReferenceLeakedException extends Exception { +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/TokenExpiredException.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/TokenExpiredException.java new file mode 100644 index 00000000..2c115e2f --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/exceptions/TokenExpiredException.java @@ -0,0 +1,26 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.exceptions; + +public class TokenExpiredException extends RuntimeException{ + + public TokenExpiredException(Throwable parent) { + super(parent); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/gui/GuiLoadingError.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/gui/GuiLoadingError.java new file mode 100644 index 00000000..f7cc5972 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/gui/GuiLoadingError.java @@ -0,0 +1,141 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.gui; + +import kr.syeyoung.dungeonsguide.launcher.util.QRCodeGenerator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.*; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.FMLCommonHandler; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Base64; +import java.util.zip.GZIPOutputStream; + +public class GuiLoadingError extends GuiScreen { + private String stacktrace; + private Throwable throwable; + + private DynamicTexture texture; + private ResourceLocation location; + private BufferedImage qrCode; + private Runnable clear; + + public GuiLoadingError(Throwable t, Runnable clear) { + this.throwable = t; + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + stacktrace = sw.toString(); + + + try { + qrCode = QRCodeGenerator.generateQRCode(stacktrace.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + texture = new DynamicTexture(qrCode.getWidth(), qrCode.getHeight()); + location = Minecraft.getMinecraft().getTextureManager().getDynamicTextureLocation("dg/errorqr", texture); + + qrCode.getRGB(0,0,qrCode.getWidth(), qrCode.getHeight(), texture.getTextureData(), 0, qrCode.getWidth()); + + texture.updateDynamicTexture(); + this.clear = clear; + } + + @Override + public void initGui() { + ScaledResolution sr = new ScaledResolution(Minecraft.getMinecraft()); + this.buttonList.add(new GuiButton(0, sr.getScaledWidth()/2-100,sr.getScaledHeight()-40 ,"Close Minecraft")); + this.buttonList.add(new GuiButton(1, sr.getScaledWidth()/2-100,sr.getScaledHeight()-70 ,"Play Without DG")); + clear.run(); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + super.actionPerformed(button); + if (button.id == 0) { + FMLCommonHandler.instance().exitJava(-1,true); + } else if (button.id == 1) { + Minecraft.getMinecraft().displayGuiScreen(null); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawBackground(1); + + ScaledResolution sr = new ScaledResolution(Minecraft.getMinecraft()); + FontRenderer fontRenderer = Minecraft.getMinecraft().fontRendererObj; + fontRenderer.drawString("DungeonsGuide has ran into unknown error while loading itself", (sr.getScaledWidth()-fontRenderer.getStringWidth("DungeonsGuide has ran into unknown error while loading itself"))/2,40,0xFFFF0000); + fontRenderer.drawString("Please contact DungeonsGuide support with this screen", (sr.getScaledWidth()-fontRenderer.getStringWidth("Please contact DungeonsGuide support with this screen"))/2, (int) (40+fontRenderer.FONT_HEIGHT*1.5),0xFFFF0000); + + int tenth = sr.getScaledWidth() / 10; + + Gui.drawRect(tenth, 70,sr.getScaledWidth()-tenth, sr.getScaledHeight()-80, 0xFF5B5B5B); + String[] split = stacktrace.split("\n"); + clip(sr, tenth, 70,sr.getScaledWidth()-2*tenth, sr.getScaledHeight()-150); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + for (int i = 0; i < split.length; i++) { + fontRenderer.drawString(split[i].replace("\t", " "), tenth+2,i*fontRenderer.FONT_HEIGHT + 72, 0xFFFFFFFF); + } + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + + GlStateManager.pushMatrix(); + GlStateManager.scale(1.0/sr.getScaleFactor(), 1.0/sr.getScaleFactor(), 1); + GlStateManager.translate(0, Minecraft.getMinecraft().displayHeight - qrCode.getHeight() * 3, 0); + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldrenderer = tessellator.getWorldRenderer(); + float f = 0.0F; + Minecraft.getMinecraft().getTextureManager().bindTexture(this.location); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate(1, 771, 0, 1); + GlStateManager.disableAlpha(); + worldrenderer.begin(7, DefaultVertexFormats.POSITION_TEX); + worldrenderer.pos(0, qrCode.getHeight()*3, 0).tex(0.0D, 1.0D).endVertex(); + worldrenderer.pos(qrCode.getWidth()*3, qrCode.getHeight()*3, 0).tex(1.0D, 1.0D).endVertex(); + worldrenderer.pos(qrCode.getWidth()*3, 0, 0).tex(1.0D, 0.0D).endVertex(); + worldrenderer.pos(0, 0, 0).tex(0.0D, 0.0D).endVertex(); + tessellator.draw(); + GlStateManager.enableAlpha(); + GlStateManager.disableBlend(); + GlStateManager.popMatrix(); + + super.drawScreen(mouseX, mouseY, partialTicks); + } + + public static void clip(ScaledResolution resolution, int x, int y, int width, int height) { + if (width < 0 || height < 0) return; + + int scale = resolution.getScaleFactor(); + GL11.glScissor((x ) * scale, Minecraft.getMinecraft().displayHeight - (y + height) * scale, (width) * scale, height * scale); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java new file mode 100644 index 00000000..ceff437e --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/IDGLoader.java @@ -0,0 +1,36 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.loader; + +import kr.syeyoung.dungeonsguide.launcher.DGInterface; +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; +import net.minecraftforge.common.config.Configuration; + +public interface IDGLoader { + void loadJar(Authenticator authenticator) throws InstantiationException, IllegalAccessException, ClassNotFoundException; + DGInterface getInstance(); + void unloadJar() throws ReferenceLeakedException; + + boolean isUnloadable(); + + boolean isLoaded(); + + String strategyName(); +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java new file mode 100644 index 00000000..2012412a --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/JarLoader.java @@ -0,0 +1,128 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.loader; + +import kr.syeyoung.dungeonsguide.launcher.DGInterface; +import kr.syeyoung.dungeonsguide.launcher.Main; +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; + +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.net.URLClassLoader; + +public class JarLoader implements IDGLoader { + private DGInterface dgInterface; + private ReferenceQueue<ClassLoader> refQueue = new ReferenceQueue<>(); + private PhantomReference<ClassLoader> phantomReference; + + private boolean loaded; + + public static class JarClassLoader extends URLClassLoader { + public JarClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + + synchronized (getClassLoadingLock(name)) { + // First, check if the class has already been loaded + Class<?> c = findLoadedClass(name); + if (c == null) { + long t0 = System.nanoTime(); + + if (c == null) { + // If still not found, then invoke findClass in order + // to find the class. + long t1 = System.nanoTime(); + c = findClass(name); + + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + try { + if (getParent() != null && c == null) { + c = getParent().loadClass(name); + } + } catch (ClassNotFoundException e) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + public Class<?> loadClassResolve(String name, boolean resolve) throws ClassNotFoundException { + return this.loadClass(name, resolve); + } + } + + @Override + public void loadJar(Authenticator authenticator) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + if (dgInterface != null) throw new IllegalStateException("Already loaded"); + + JarClassLoader classLoader = new JarClassLoader(new URL[] { + Main.class.getResource("/mod.jar") + }, this.getClass().getClassLoader()); + + dgInterface = (DGInterface) classLoader.loadClassResolve("kr.syeyoung.dungeonsguide.DungeonsGuide", true).newInstance(); + phantomReference = new PhantomReference<>(classLoader, refQueue); + } + + @Override + public DGInterface getInstance() { + return dgInterface; + } + + @Override + public void unloadJar() throws ReferenceLeakedException { + dgInterface.unload(); + dgInterface = null; + System.gc();// pls do + Reference<? extends ClassLoader> t = refQueue.poll(); + if (t == null) throw new ReferenceLeakedException(); // Why do you have to be that strict? Well, to tell them to actually listen on DungeonsGuideReloadListener. If it starts causing issues then I will remove check cus it's not really loaded (classes are loaded by child classloader) + t.clear(); + phantomReference = null; + } + + @Override + public boolean isUnloadable() { + return true; + } + + @Override + public boolean isLoaded() { + return dgInterface != null; + } + + @Override + public String strategyName() { + return "jar"; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java new file mode 100644 index 00000000..67c43ad5 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/loader/LocalLoader.java @@ -0,0 +1,58 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.loader; + +import kr.syeyoung.dungeonsguide.launcher.DGInterface; +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import kr.syeyoung.dungeonsguide.launcher.exceptions.ReferenceLeakedException; + +public class LocalLoader implements IDGLoader { + private DGInterface dgInterface; + + @Override + public void loadJar(Authenticator authenticator) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + if (dgInterface != null) throw new IllegalStateException("Already loaded"); + dgInterface = (DGInterface) Class.forName("kr.syeyoung.dungeonsguide.DungeonsGuide").newInstance(); + } + + @Override + public DGInterface getInstance() { + return dgInterface; + } + + @Override + public void unloadJar() throws ReferenceLeakedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnloadable() { + return false; + } + + @Override + public boolean isLoaded() { + return dgInterface != null; + } + + @Override + public String strategyName() { + return "local"; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGConnection.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGConnection.java new file mode 100755 index 00000000..649dfdc5 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGConnection.java @@ -0,0 +1,47 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.url; + +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; + +import java.io.*; +import java.net.URL; +import java.net.URLConnection; + +public class DGConnection extends URLConnection { + private final Authenticator authenticator; + protected DGConnection(URL url, Authenticator a) { + super(url); + connected = false; + this.authenticator = a; + } + + @Override + public void connect() throws IOException { + } + @Override + public InputStream getInputStream() throws IOException { + if (authenticator != null) { + String path = url.getPath().substring(1); +// if (!authenticator.getResources().containsKey(path)) throw new FileNotFoundException(); +// return new ByteArrayInputStream(authenticator.getResources().get(path)); + } + throw new FileNotFoundException(); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandler.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandler.java new file mode 100755 index 00000000..82ddff18 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandler.java @@ -0,0 +1,36 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.url; + +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import lombok.AllArgsConstructor; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +@AllArgsConstructor +public class DGStreamHandler extends URLStreamHandler { + private final Authenticator auth; + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new DGConnection(url, this.auth); + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandlerFactory.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandlerFactory.java new file mode 100755 index 00000000..a7b50046 --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/url/DGStreamHandlerFactory.java @@ -0,0 +1,37 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.url; + +import kr.syeyoung.dungeonsguide.launcher.authentication.Authenticator; +import lombok.AllArgsConstructor; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +@AllArgsConstructor +public class DGStreamHandlerFactory implements URLStreamHandlerFactory { + private final Authenticator auth; + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if ("z".equals(protocol)) { + return new DGStreamHandler(this.auth); + } + return null; + } +} diff --git a/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/util/QRCodeGenerator.java b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/util/QRCodeGenerator.java new file mode 100644 index 00000000..d0c3306a --- /dev/null +++ b/loader/src/main/java/kr/syeyoung/dungeonsguide/launcher/util/QRCodeGenerator.java @@ -0,0 +1,43 @@ +/* + * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod + * Copyright (C) 2021 cyoung06 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package kr.syeyoung.dungeonsguide.launcher.util; + +import io.nayuki.qrcodegen.QrCode; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPOutputStream; + +public class QRCodeGenerator { + public static BufferedImage generateQRCode(byte[] bytes) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); + gzipOutputStream.write(bytes); + gzipOutputStream.flush(); + gzipOutputStream.close(); + + + byte[] array = byteArrayOutputStream.toByteArray(); + + QrCode qrCode = QrCode.encodeBinary(array, QrCode.Ecc.LOW); + return qrCode.toImage(1, 0); + } +} diff --git a/loader/src/main/resources/mcmod.info b/loader/src/main/resources/mcmod.info new file mode 100755 index 00000000..70df65aa --- /dev/null +++ b/loader/src/main/resources/mcmod.info @@ -0,0 +1,16 @@ +[ +{ + "modid": "skyblock_dungeons_guide", + "name": "Skyblock Dungeons Guide", + "description": "A mod to help dungeon players to find and solve secrets and puzzles most efficiently.", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "", + "updateUrl": "", + "authorList": ["syeyoung"], + "credits": "The guild Jerry's Crew, for nothing.", + "logoFile": "", + "screenshots": [], + "dependencies": [] +} +] |