From 1ab63d2bc5a25a30ade1f21975bb6b0cb6e6d14a Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Fri, 19 Jan 2024 14:06:07 +0100 Subject: Add crash recovery page to PV (#999) * Add crash recovery page to PV * Add warning for missing auth token --- .../profileviewer/CrashRecoveryPage.java | 147 +++++++++++++++++++++ .../profileviewer/GuiProfileViewer.java | 137 ++++++++++++++----- .../moulberry/notenoughupdates/util/Rectangle.kt | 8 +- .../moulberry/notenoughupdates/util/UrsaClient.kt | 30 ++++- 4 files changed, 285 insertions(+), 37 deletions(-) create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/profileviewer/CrashRecoveryPage.java (limited to 'src/main') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/CrashRecoveryPage.java b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/CrashRecoveryPage.java new file mode 100644 index 00000000..d68f4a26 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/CrashRecoveryPage.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2024 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see . + */ + +package io.github.moulberry.notenoughupdates.profileviewer; + +import io.github.moulberry.moulconfig.internal.ClipboardUtils; +import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils; +import io.github.moulberry.notenoughupdates.util.Rectangle; +import io.github.moulberry.notenoughupdates.util.Utils; +import lombok.val; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.crash.CrashReport; +import net.minecraft.init.Bootstrap; + +import java.io.IOException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class CrashRecoveryPage extends GuiProfileViewerPage { + private final Exception exception; + private final String timestamp; + private final GuiProfileViewer.ProfileViewerPage lastViewedPage; + private int offset = 0; + private final CrashReport crashReport; + + public CrashRecoveryPage( + GuiProfileViewer instance, + Exception exception, + GuiProfileViewer.ProfileViewerPage lastViewedPage + ) { + super(instance); + this.lastViewedPage = lastViewedPage; + this.timestamp = DateTimeFormatter.ISO_ZONED_DATE_TIME.format(OffsetDateTime.ofInstant( + Instant.now(), + ZoneId.systemDefault() + )); + this.exception = exception; + val profile = GuiProfileViewer.getProfile(); + crashReport = new CrashReport("NEU Profile Viewer crashed", exception); + val parameters = crashReport.makeCategory("Profile Viewer Parameters"); + + parameters.addCrashSection("Viewed Player", (profile == null ? "null" : profile.getUuid())); + parameters.addCrashSection("Viewed Profile", GuiProfileViewer.getProfileName()); + parameters.addCrashSection("Timestamp", timestamp); + parameters.addCrashSection("Last Viewed Page", lastViewedPage); + Bootstrap.printToSYSOUT(crashReport.getCompleteReport()); + } + + @Override + public void drawPage(int mouseX, int mouseY, float partialTicks) { + GlStateManager.pushMatrix(); + GlStateManager.translate( + GuiProfileViewer.getGuiLeft() + getInstance().sizeX / 2f, + GuiProfileViewer.getGuiTop() + 20, + 0 + ); + offset = 20; + + drawTitle(); + + drawString("§cLooked like your profile viewer crashed."); + drawString("§cPlease immediately send a screenshot of this screen into #neu-support."); + drawString("§cJoin our support server at §adiscord.gg/moulberry§c."); + + val profile = GuiProfileViewer.getProfile(); + drawString("Viewed Player: " + (profile == null ? "null" : profile.getUuid())); + drawString("Viewed Profile: " + GuiProfileViewer.getProfileName()); + drawString("Timestamp: " + timestamp); + + drawString(""); + drawString(exception.toString()); + for (StackTraceElement stackTraceElement : exception.getStackTrace()) { + if (offset >= getInstance().sizeY - 50) break; + drawString(stackTraceElement.toString()); + } + + GlStateManager.popMatrix(); + + val buttonCoords = getButtonCoordinates(); + RenderUtils.drawFloatingRectWithAlpha( + buttonCoords.getX(), buttonCoords.getY(), + buttonCoords.getWidth(), buttonCoords.getHeight(), + 100, true + ); + Utils.drawStringCenteredScaledMaxWidth( + "Copy Report", + buttonCoords.getCenterX(), + buttonCoords.getCenterY(), + false, + buttonCoords.getWidth(), + -1 + ); + + } + + @Override + public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + if (getButtonCoordinates().contains(mouseX, mouseY) && mouseButton == 0) { + ClipboardUtils.copyToClipboard(crashReport.getCompleteReport()); + } + return super.mouseClicked(mouseX, mouseY, mouseButton); + } + + private Rectangle getButtonCoordinates() { + return new Rectangle( + GuiProfileViewer.getGuiLeft() + getInstance().sizeX / 2 - 40, + GuiProfileViewer.getGuiTop() + getInstance().sizeY - 30, + 80, 12 + ); + } + + private void drawString(String text) { + Utils.drawStringCenteredScaledMaxWidth(text, 0, 0, false, getInstance().sizeX - 20, -1); + val spacing = Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT + 2; + GlStateManager.translate(0, spacing, 0); + offset += spacing; + } + + private void drawTitle() { + GlStateManager.pushMatrix(); + GlStateManager.scale(2, 2, 2); + Utils.drawStringCenteredScaledMaxWidth("§cKA-BOOM!", 0, 0, false, getInstance().sizeX / 2, -1); + GlStateManager.popMatrix(); + val spacing = Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT * 2 + 6; + GlStateManager.translate(0, spacing, 0); + offset += spacing; + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/GuiProfileViewer.java b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/GuiProfileViewer.java index f3c96854..6a734958 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/GuiProfileViewer.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/profileviewer/GuiProfileViewer.java @@ -32,7 +32,9 @@ import io.github.moulberry.notenoughupdates.profileviewer.weight.weight.SkillsWe import io.github.moulberry.notenoughupdates.profileviewer.weight.weight.Weight; import io.github.moulberry.notenoughupdates.util.AsyncDependencyLoader; import io.github.moulberry.notenoughupdates.util.PronounDB; +import io.github.moulberry.notenoughupdates.util.UrsaClient; import io.github.moulberry.notenoughupdates.util.Utils; +import lombok.val; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityOtherPlayerMP; import net.minecraft.client.gui.GuiScreen; @@ -199,23 +201,28 @@ public class GuiProfileViewer extends GuiScreen { playerNameTextField = new GuiElementTextField(displayname, GuiElementTextField.SCALE_TEXT); playerNameTextField.setSize(100, 20); - if (currentPage == ProfileViewerPage.LOADING) { + if (currentPage.isTransient()) { currentPage = ProfileViewerPage.BASIC; } - pages.put(ProfileViewerPage.BASIC, new BasicPage(this)); - pages.put(ProfileViewerPage.DUNGEON, new DungeonPage(this)); - pages.put(ProfileViewerPage.EXTRA, new ExtraPage(this)); - pages.put(ProfileViewerPage.INVENTORIES, new InventoriesPage(this)); - pages.put(ProfileViewerPage.COLLECTIONS, new CollectionsPage(this)); - pages.put(ProfileViewerPage.PETS, new PetsPage(this)); - pages.put(ProfileViewerPage.MINING, new MiningPage(this)); - pages.put(ProfileViewerPage.BINGO, new BingoPage(this)); - pages.put(ProfileViewerPage.TROPHY_FISH, new TrophyFishPage(this)); - pages.put(ProfileViewerPage.BESTIARY, new BestiaryPage(this)); - pages.put(ProfileViewerPage.CRIMSON_ISLE, new CrimsonIslePage(this)); - pages.put(ProfileViewerPage.MUSEUM, new MuseumPage(this)); - pages.put(ProfileViewerPage.RIFT, new RiftPage(this)); + try { + pages.put(ProfileViewerPage.BASIC, new BasicPage(this)); + pages.put(ProfileViewerPage.DUNGEON, new DungeonPage(this)); + pages.put(ProfileViewerPage.EXTRA, new ExtraPage(this)); + pages.put(ProfileViewerPage.INVENTORIES, new InventoriesPage(this)); + pages.put(ProfileViewerPage.COLLECTIONS, new CollectionsPage(this)); + pages.put(ProfileViewerPage.PETS, new PetsPage(this)); + pages.put(ProfileViewerPage.MINING, new MiningPage(this)); + pages.put(ProfileViewerPage.BINGO, new BingoPage(this)); + pages.put(ProfileViewerPage.TROPHY_FISH, new TrophyFishPage(this)); + pages.put(ProfileViewerPage.BESTIARY, new BestiaryPage(this)); + pages.put(ProfileViewerPage.CRIMSON_ISLE, new CrimsonIslePage(this)); + pages.put(ProfileViewerPage.MUSEUM, new MuseumPage(this)); + pages.put(ProfileViewerPage.RIFT, new RiftPage(this)); + } catch (Exception ex) { + pages.put(ProfileViewerPage.CRASH_RECOVERY, new CrashRecoveryPage(this, ex, currentPage)); + currentPage = ProfileViewerPage.CRASH_RECOVERY; + } } public static int getGuiLeft() { @@ -244,15 +251,17 @@ public class GuiProfileViewer extends GuiScreen { if (startTime == 0) startTime = currentTime; ProfileViewerPage page = currentPage; - if (profile == null) { - page = ProfileViewerPage.INVALID_NAME; - } else if (profile.getOrLoadSkyblockProfiles(null) == null) { - page = ProfileViewerPage.LOADING; - } + if (page == ProfileViewerPage.CRASH_RECOVERY) { + if (profile == null) { + page = ProfileViewerPage.INVALID_NAME; + } else if (profile.getOrLoadSkyblockProfiles(null) == null) { + page = ProfileViewerPage.LOADING; + } - if (profile != null && profile.getLatestProfileName() == null && - !profile.getUpdatingSkyblockProfilesState().get()) { - page = ProfileViewerPage.NO_SKYBLOCK; + if (profile != null && profile.getLatestProfileName() == null && + !profile.getUpdatingSkyblockProfilesState().get()) { + page = ProfileViewerPage.NO_SKYBLOCK; + } } if (profile != null) { @@ -271,7 +280,8 @@ public class GuiProfileViewer extends GuiScreen { if (NotEnoughUpdates.INSTANCE.config.profileViewer.alwaysShowBingoTab) { showBingoPage = true; } else { - showBingoPage = selectedProfile != null && selectedProfile.getGamemode() != null && selectedProfile.getGamemode().equals("bingo"); + showBingoPage = selectedProfile != null && selectedProfile.getGamemode() != null && + selectedProfile.getGamemode().equals("bingo"); } if (!showBingoPage && currentPage == ProfileViewerPage.BINGO) { @@ -326,7 +336,10 @@ public class GuiProfileViewer extends GuiScreen { if (selectedProfile != null && selectedProfile.getGamemode() != null) { GlStateManager.color(1, 1, 1, 1); - ResourceLocation gamemodeIcon = gamemodeToIcon.getOrDefault(selectedProfile.getGamemode(), gamemodeIconUnknown); + ResourceLocation gamemodeIcon = gamemodeToIcon.getOrDefault( + selectedProfile.getGamemode(), + gamemodeIconUnknown + ); Minecraft.getMinecraft().getTextureManager().bindTexture(gamemodeIcon); Utils.drawTexturedRect(guiLeft - 16 - 5, guiTop + sizeY + 5, 16, 16, GL11.GL_NEAREST); } @@ -398,7 +411,10 @@ public class GuiProfileViewer extends GuiScreen { if (selectedProfile != null && selectedProfile.getGamemode() != null) { GlStateManager.color(1, 1, 1, 1); - ResourceLocation gamemodeIcon = gamemodeToIcon.getOrDefault(selectedProfile.getGamemode(), gamemodeIconUnknown); + ResourceLocation gamemodeIcon = gamemodeToIcon.getOrDefault( + selectedProfile.getGamemode(), + gamemodeIconUnknown + ); Minecraft.getMinecraft().getTextureManager().bindTexture(gamemodeIcon); Utils.drawTexturedRect( guiLeft - 16 - 5, @@ -416,7 +432,14 @@ public class GuiProfileViewer extends GuiScreen { GlStateManager.color(1, 1, 1, 1); if (pages.containsKey(page)) { - pages.get(page).drawPage(mouseX, mouseY, partialTicks); + try { + pages.get(page).drawPage(mouseX, mouseY, partialTicks); + } catch (Exception ex) { + if (page == ProfileViewerPage.CRASH_RECOVERY) + throw ex; // Rethrow if the exception handler crashes. + pages.put(ProfileViewerPage.CRASH_RECOVERY, new CrashRecoveryPage(this, ex, currentPage)); + currentPage = ProfileViewerPage.CRASH_RECOVERY; + } } else { switch (page) { case LOADING: @@ -436,7 +459,29 @@ public class GuiProfileViewer extends GuiScreen { //like telling them to go find a psychotherapist long timeDiff = System.currentTimeMillis() - startTime; - if (timeDiff > 20000) { + val authState = NotEnoughUpdates.INSTANCE.manager.ursaClient.getAuthenticationState(); + if (authState == UrsaClient.AuthenticationState.FAILED_TO_JOINSERVER) { + Utils.drawStringCentered( + EnumChatFormatting.RED + + "Looks like we cannot authenticate with Mojang.", + guiLeft + sizeX / 2f, guiTop + 111, true, 0 + ); + Utils.drawStringCentered( + EnumChatFormatting.RED + "Is your game open for more than 24 hours?", + guiLeft + sizeX / 2f, guiTop + 121, true, 0 + ); + } else if (authState == UrsaClient.AuthenticationState.REJECTED) { + Utils.drawStringCentered( + EnumChatFormatting.RED + + "Looks like we cannot authenticate with Ursa.", + guiLeft + sizeX / 2f, guiTop + 111, true, 0 + ); + Utils.drawStringCentered( + EnumChatFormatting.RED + "Is your game open for more than 24 hours?", + guiLeft + sizeX / 2f, guiTop + 121, true, 0 + ); + + } else if (timeDiff > 20000) { Utils.drawStringCentered( EnumChatFormatting.YELLOW + "Its taking a while...", guiLeft + sizeX / 2f, guiTop + 111, true, 0 @@ -466,7 +511,8 @@ public class GuiProfileViewer extends GuiScreen { guiLeft + sizeX / 2f, guiTop + 151, true, 0 ); Utils.drawStringCentered( - EnumChatFormatting.YELLOW + String.valueOf(EnumChatFormatting.BOLD) + "What are you doing with your life?", + EnumChatFormatting.YELLOW + String.valueOf(EnumChatFormatting.BOLD) + + "What are you doing with your life?", guiLeft + sizeX / 2f, guiTop + 161, true, 0 ); if (timeDiff > 600000) { @@ -476,7 +522,8 @@ public class GuiProfileViewer extends GuiScreen { ); if (timeDiff > 1200000) { Utils.drawStringCentered( - EnumChatFormatting.RED + String.valueOf(EnumChatFormatting.BOLD) + "You're a menace to society", + EnumChatFormatting.RED + String.valueOf(EnumChatFormatting.BOLD) + + "You're a menace to society", guiLeft + sizeX / 2f, guiTop + 181, true, 0 ); if (timeDiff > 1800000) { @@ -487,7 +534,8 @@ public class GuiProfileViewer extends GuiScreen { ); if (timeDiff > 3000000) { Utils.drawStringCentered( - EnumChatFormatting.RED + String.valueOf(EnumChatFormatting.BOLD) + "You really want this?", + EnumChatFormatting.RED + String.valueOf(EnumChatFormatting.BOLD) + + "You really want this?", guiLeft + sizeX / 2f, guiTop + 91, true, 0 ); if (timeDiff > 3300000) { @@ -695,7 +743,7 @@ public class GuiProfileViewer extends GuiScreen { } if (selected) { - uMin = 196 /256f; + uMin = 196 / 256f; uMax = 226 / 256f; renderBlurredBackground(width, height, x - 2, y + 2, 30 - 2, 28 - 4); @@ -750,8 +798,14 @@ public class GuiProfileViewer extends GuiScreen { } if (pages.containsKey(currentPage)) { - if (pages.get(currentPage).mouseClicked(mouseX, mouseY, mouseButton)) { - return; + try { + if (pages.get(currentPage).mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + } catch (Exception ex) { + if (currentPage == ProfileViewerPage.CRASH_RECOVERY) throw ex; + pages.put(ProfileViewerPage.CRASH_RECOVERY, new CrashRecoveryPage(this, ex, currentPage)); + currentPage = ProfileViewerPage.CRASH_RECOVERY; } } @@ -869,7 +923,13 @@ public class GuiProfileViewer extends GuiScreen { super.keyTyped(typedChar, keyCode); if (pages.containsKey(currentPage)) { - pages.get(currentPage).keyTyped(typedChar, keyCode); + try { + pages.get(currentPage).keyTyped(typedChar, keyCode); + } catch (Exception ex) { + if (currentPage == ProfileViewerPage.CRASH_RECOVERY) throw ex; + pages.put(ProfileViewerPage.CRASH_RECOVERY, new CrashRecoveryPage(this, ex, currentPage)); + currentPage = ProfileViewerPage.CRASH_RECOVERY; + } } if (playerNameTextField.getFocus()) { @@ -917,7 +977,9 @@ public class GuiProfileViewer extends GuiScreen { if (levelObj.maxed) { renderGoldBar(x, y + 6, xSize); } else { - if ((skillName.contains("Catacombs") || Weight.DUNGEON_CLASS_NAMES.stream().anyMatch(e -> skillName.toLowerCase().contains(e))) && levelObj.level >= 50) { + if ((skillName.contains("Catacombs") || Weight.DUNGEON_CLASS_NAMES.stream().anyMatch(e -> skillName + .toLowerCase() + .contains(e))) && levelObj.level >= 50) { renderGoldBar(x, y + 6, xSize); } else { renderBar(x, y + 6, xSize, level % 1); @@ -1202,6 +1264,7 @@ public class GuiProfileViewer extends GuiScreen { LOADING(), INVALID_NAME(), NO_SKYBLOCK(), + CRASH_RECOVERY(), BASIC(0, Items.paper, "§9Skills"), DUNGEON(1, Item.getItemFromBlock(Blocks.deadbush), "§eDungeoneering"), EXTRA(2, Items.book, "§7Profile Stats"), @@ -1246,6 +1309,10 @@ public class GuiProfileViewer extends GuiScreen { return null; } + public boolean isTransient() { + return this == LOADING || this == NO_SKYBLOCK || this == INVALID_NAME || this == CRASH_RECOVERY; + } + public Optional getItem() { return Optional.ofNullable(stack); } diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt index d44b7721..1882dac4 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/Rectangle.kt @@ -31,6 +31,12 @@ data class Rectangle( val x: Int, val y: Int, val width: Int, val height: Int, ) { + val centerX: Int + get() = x + width / 2 + val centerY: Int + get() = y + height / 2 + + /** * The left edge of this rectangle (Low X) */ @@ -69,7 +75,7 @@ data class Rectangle( /** * Check if this rectangle contains the given coordinate */ - fun contains(x1: Int, y1: Int) :Boolean{ + fun contains(x1: Int, y1: Int): Boolean { return left <= x1 && x1 < left + width && top <= y1 && y1 < top + height } } diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt index 312c5d9b..67df5bf9 100644 --- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/UrsaClient.kt @@ -20,6 +20,7 @@ package io.github.moulberry.notenoughupdates.util import com.google.gson.JsonObject +import com.mojang.authlib.exceptions.AuthenticationException import io.github.moulberry.notenoughupdates.NotEnoughUpdates import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag @@ -92,6 +93,7 @@ class UrsaClient(val apiUtil: ApiUtil) { } else { this.token = Token(validUntil, token, usedUrsaRoot) isPollingForToken = false + authenticationState = AuthenticationState.SUCCEEDED logger.log("Token saving successful") } } @@ -115,6 +117,13 @@ class UrsaClient(val apiUtil: ApiUtil) { logger.log("Request failed") continueOn(MinecraftExecutor.OnThread) isPollingForToken = false + if (e is AuthenticationException) { + authenticationState = AuthenticationState.FAILED_TO_JOINSERVER + } + if (e is HttpStatusCodeException && e.statusCode == 401) { + authenticationState = AuthenticationState.REJECTED + this.token = null + } request.consumer.completeExceptionally(e) } } @@ -179,6 +188,24 @@ class UrsaClient(val apiUtil: ApiUtil) { } } + + private var authenticationState = AuthenticationState.NOT_ATTEMPTED + + fun getAuthenticationState(): AuthenticationState { + if (authenticationState == AuthenticationState.SUCCEEDED && token?.isValid != true) { + return AuthenticationState.OUTDATED + } + return authenticationState + } + + enum class AuthenticationState { + NOT_ATTEMPTED, + FAILED_TO_JOINSERVER, + REJECTED, + SUCCEEDED, + OUTDATED, + } + companion object { @JvmStatic fun profiles(uuid: UUID) = KnownRequest("v1/hypixel/v2/profiles/${uuid}", JsonObject::class.java) @@ -193,7 +220,8 @@ class UrsaClient(val apiUtil: ApiUtil) { fun bingo(uuid: UUID) = KnownRequest("v1/hypixel/v2/bingo/${uuid}", JsonObject::class.java) @JvmStatic - fun museumForProfile(profileUuid: String) = KnownRequest("v1/hypixel/v2/museum/${profileUuid}", JsonObject::class.java) + fun museumForProfile(profileUuid: String) = + KnownRequest("v1/hypixel/v2/museum/${profileUuid}", JsonObject::class.java) @JvmStatic fun status(uuid: UUID) = KnownRequest("v1/hypixel/v2/status/${uuid}", JsonObject::class.java) -- cgit