diff --git a/build.gradle b/build.gradle index 826f675..7b3637a 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,8 @@ dependencies { } include(implementation("org.xerial:sqlite-jdbc:3.46.1.3")) + include(clientImplementation("com.google.zxing:core:${project.zxing_version}")) + include(clientImplementation("com.google.zxing:javase:${project.zxing_version}")) } processResources { diff --git a/gradle.properties b/gradle.properties index 7813374..c9a7aa2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,10 +6,11 @@ minecraft_version=1.21.11 yarn_mappings=1.21.11+build.4 loader_version=0.18.1 # Mod Properties -mod_version=0.1-pre-alpha +mod_version=0.2-pre-alpha maven_group=git.yawaflua.tech archives_base_name=SPMega # Dependencies # check this on https://modmuss50.me/fabric.html fabric_version=0.141.1+1.21.11 cloth_config_version=21.11.153 +zxing_version=3.5.3 diff --git a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java index 515ceaa..ee06332 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/SPMegaClient.java @@ -1,17 +1,23 @@ package git.yawaflua.tech.spmega.client; import git.yawaflua.tech.spmega.SPMega; +import git.yawaflua.tech.spmega.client.qr.QRCodeScanner; +import git.yawaflua.tech.spmega.client.ui.UiNotifications; import git.yawaflua.tech.spmega.client.ui.UiOpeners; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; import net.minecraft.block.entity.SignBlockEntity; import net.minecraft.client.MinecraftClient; import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; +import net.minecraft.entity.decoration.ItemFrameEntity; import net.minecraft.text.Text; import net.minecraft.util.ActionResult; import org.lwjgl.glfw.GLFW; @@ -22,6 +28,7 @@ import java.util.regex.Pattern; public class SPMegaClient implements ClientModInitializer { private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(? { + UiNotifications.instance().tick(); while (openBankMenuKeyBinding.wasPressed()) { UiOpeners.openMainMenu(client); } + while (scanQrKeyBinding.wasPressed()) { + QRCodeScanner.ScanQrCode(client); + } }); + // World HUD path (when no screen is open). + HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.currentScreen != null || client.textRenderer == null) { + return; + } + UiNotifications.instance().render( + drawContext, + client.textRenderer, + client.getWindow().getScaledWidth(), + client.getWindow().getScaledHeight() + ); + }); + + // Screen path (for all GUI screens). + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> + ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> { + if (client.textRenderer == null) { + return; + } + UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight); + }) + ); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { if (client.player != null) { BankUiService.instance().refreshOnServerJoin(client.player.getUuidAsString()); @@ -73,17 +130,42 @@ public class SPMegaClient implements ClientModInitializer { return ActionResult.PASS; } - if (!(world.getBlockEntity(hitResult.getBlockPos()) instanceof SignBlockEntity signBlockEntity)) { + if (world.getBlockEntity(hitResult.getBlockPos()) instanceof SignBlockEntity signBlockEntity) { + String cardNumber = extractCardNumber(signBlockEntity); + if (cardNumber == null) { + return ActionResult.PASS; + } + + UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber); + return ActionResult.SUCCESS; + } else { return ActionResult.PASS; } - String cardNumber = extractCardNumber(signBlockEntity); - if (cardNumber == null) { + }); + + UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (!world.isClient()) { + return ActionResult.PASS; + } + if (!player.isSneaking()) { + return ActionResult.PASS; + } + if (SPMega.getConfig() == null || !SPMega.getConfig().signQuickPayEnabled()) { + return ActionResult.PASS; + } + if (entity instanceof ItemFrameEntity itemFrameEntity) { + String cardNumber = extractCardNumber(itemFrameEntity); + if (cardNumber == null) { + return ActionResult.PASS; + } + + UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber); + return ActionResult.SUCCESS; + } else { return ActionResult.PASS; } - UiOpeners.openPaymentMenu(MinecraftClient.getInstance(), cardNumber); - return ActionResult.SUCCESS; }); } } diff --git a/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java b/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java new file mode 100644 index 0000000..b41399c --- /dev/null +++ b/src/client/java/git/yawaflua/tech/spmega/client/qr/QRCodeScanner.java @@ -0,0 +1,131 @@ +package git.yawaflua.tech.spmega.client.qr; + +import com.google.zxing.*; +import com.google.zxing.common.GlobalHistogramBinarizer; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.multi.GenericMultipleBarcodeReader; +import git.yawaflua.tech.spmega.client.ui.QRcodeAcceptScreen; +import git.yawaflua.tech.spmega.client.ui.UiNotifications; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.util.ScreenshotRecorder; +import net.minecraft.text.Text; + +import java.awt.image.BufferedImage; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; + +public class QRCodeScanner { + + public static void ScanQrCode(MinecraftClient client) { + if (client == null || client.player == null || client.getFramebuffer() == null) { + return; + } + UiNotifications notifications = UiNotifications.instance(); + int framebufferWidth = client.getWindow().getFramebufferWidth(); + int framebufferHeight = client.getWindow().getFramebufferHeight(); + if (framebufferWidth <= 0 || framebufferHeight <= 0) { + client.player.sendMessage(Text.translatable("message.spmega.qr.capture_failed"), false); + return; + } + + try { + ScreenshotRecorder.takeScreenshot(client.getFramebuffer(), nativeImage -> { + try { + if (nativeImage == null || nativeImage.getWidth() <= 0 || nativeImage.getHeight() <= 0) { + client.execute(() -> { + if (client.player != null) { + notifications.show(Text.translatable("message.spmega.qr.capture_failed")); + } + }); + return; + } + + String result = decodeQRCode(nativeImageToBufferedImage(nativeImage)); + client.execute(() -> { + if (client.player == null) { + return; + } + if (result == null) { + notifications.show(Text.translatable("message.spmega.qr.not_found")); + return; + } + + Text clickableLink = Text.literal(result) + .styled(style -> style.withInsertion(result)); + client.player.sendMessage(Text.translatable("message.spmega.qr.found_link", clickableLink), false); + client.setScreen(new QRcodeAcceptScreen(result, client.currentScreen)); + }); + } finally { + if (nativeImage != null) { + nativeImage.close(); + } + } + }); + } catch (Exception ex) { + notifications.show(Text.translatable("message.spmega.qr.error")); + } + } + + private static BufferedImage nativeImageToBufferedImage(NativeImage screenshot) { + BufferedImage bufferedImage = new BufferedImage( + screenshot.getWidth(), + screenshot.getHeight(), + BufferedImage.TYPE_INT_RGB + ); + + for (int y = 0; y < screenshot.getHeight(); y++) { + for (int x = 0; x < screenshot.getWidth(); x++) { + int color = screenshot.getColorArgb(x, y); + bufferedImage.setRGB(x, y, color); + } + } + + return bufferedImage; + } + + private static String decodeQRCode(BufferedImage image) { + Map hints = new EnumMap<>(DecodeHintType.class); + hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); + hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE)); + hints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); + hints.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE); + + LuminanceSource source = new RGBLuminanceSource(image.getWidth(), image.getHeight(), image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth())); + + String result = tryDecodeWithStrategies(source, hints); + + if (result == null) { + hints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE); + result = tryDecodeWithStrategies(source, hints); + } + + return result; + } + + private static String tryDecodeWithStrategies(LuminanceSource source, Map hints) { + try { + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + return new MultiFormatReader().decode(bitmap, hints).getText(); + } catch (NotFoundException e) { + } + + try { + BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source)); + return new MultiFormatReader().decode(bitmap, hints).getText(); + } catch (NotFoundException e) { + } + try { + GenericMultipleBarcodeReader reader = new GenericMultipleBarcodeReader(new MultiFormatReader()); + Result[] results = reader.decodeMultiple(new BinaryBitmap(new HybridBinarizer(source)), hints); + if (results.length > 0) { + return results[0].getText(); + } + } catch (NotFoundException e) { + } + + return null; + } +} + diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java index 012f1f6..1165a1b 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/AddCardScreen.java @@ -67,7 +67,6 @@ public class AddCardScreen extends Screen { context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF); context.drawTextWithShadow(this.textRenderer, Text.literal("Card ID (UUID):"), centerX - 140, startY - 10, 0xCCCCCC); context.drawTextWithShadow(this.textRenderer, Text.literal("Card Token:"), centerX - 140, startY + 18, 0xCCCCCC); - notifications.render(context, this.textRenderer, this.width, this.height); } private void submit() { diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java index 7d1171c..a0cd6b4 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/CardScreen.java @@ -99,7 +99,6 @@ public class CardScreen extends Screen { context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 24, 0xFFFFFF); context.drawTextWithShadow(this.textRenderer, Text.literal("Список карт"), leftX, startY - 18, 0xBFBFBF); context.drawTextWithShadow(this.textRenderer, Text.literal("Действия"), rightX, startY - 18, 0xBFBFBF); - notifications.render(context, this.textRenderer, this.width, this.height); } private void updateCardButtonStates() { diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/MainBankScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/MainBankScreen.java index 7edd40d..c4c5a3e 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/MainBankScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/MainBankScreen.java @@ -1,11 +1,14 @@ package git.yawaflua.tech.spmega.client.ui; +import git.yawaflua.tech.spmega.client.qr.QRCodeScanner; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.text.Text; +import java.awt.*; + public class MainBankScreen extends Screen { private final Screen parent; @@ -34,6 +37,11 @@ public class MainBankScreen extends Screen { this.addDrawableChild(ButtonWidget.builder(Text.literal("Закрыть"), button -> this.close()) .dimensions(startX + (buttonWidth + gap) * 2, y, buttonWidth, 20) .build()); + + this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.scan_qr"), button -> { + this.client.setScreen(null); + QRCodeScanner.ScanQrCode(this.client); + }).dimensions(centerX - 60, y + 28, 120, 20).build()); } @Override @@ -48,7 +56,7 @@ public class MainBankScreen extends Screen { super.render(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 24, 0xFFFFFF); - context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(greetingLine()), this.width / 2, 48, 0xFFD27F); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(greetingLine()), this.width / 2, 48, Color.WHITE.getRGB()); } private String greetingLine() { diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java index 9175033..668b13f 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/PaymentScreen.java @@ -189,7 +189,6 @@ public class PaymentScreen extends Screen { } context.drawCenteredTextWithShadow(this.textRenderer, senderCardText, centerX, this.height - 20, 0xA9E5A9); - notifications.render(context, this.textRenderer, this.width, this.height); } private void submit() { diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java new file mode 100644 index 0000000..df15417 --- /dev/null +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/QRcodeAcceptScreen.java @@ -0,0 +1,70 @@ +package git.yawaflua.tech.spmega.client.ui; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +import java.net.URI; + +public class QRcodeAcceptScreen extends Screen { + private final String url; + private final Screen parent; + + public QRcodeAcceptScreen(String url, Screen parent) { + super(Text.translatable("screen.spmega.qr.confirm_title")); + this.url = url; + this.parent = parent; + } + + @Override + protected void init() { + this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> { + if (this.client != null) { + this.client.setScreen(parent); + } + }).dimensions(this.width / 2 - 155, this.height / 2 + 30, 150, 20).build()); + + this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.open_link"), button -> { + try { + Util.getOperatingSystem().open(new URI(url)); + } catch (Exception exception) { + if (this.client != null && this.client.player != null) { + this.client.player.sendMessage(Text.translatable("message.spmega.qr.failed_open"), false); + } + } + if (this.client != null) { + this.client.setScreen(parent); + } + }).dimensions(this.width / 2 + 5, this.height / 2 + 30, 150, 20).build()); + } + + @Override + public void close() { + if (this.client != null) { + this.client.setScreen(parent); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.translatable("screen.spmega.qr.accept_link"), + this.width / 2, + this.height / 2 - 30, + 0xFFFFFF + ); + context.drawCenteredTextWithShadow( + this.textRenderer, + Text.literal(url), + this.width / 2, + this.height / 2 - 10, + 0xFFD27F + ); + } +} + diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/UiNotifications.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/UiNotifications.java index b882681..072f6d2 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/UiNotifications.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/UiNotifications.java @@ -12,11 +12,11 @@ import net.minecraft.text.Text; import java.awt.*; public final class UiNotifications { - private static final long DEFAULT_DURATION_MS = 3500L; + private static final int DEFAULT_DURATION_TICKS = 70; private static final UiNotifications INSTANCE = new UiNotifications(); private Text currentText = Text.empty(); - private long visibleUntilMs; + private int remainingTicks; private UiNotifications() { } @@ -31,7 +31,6 @@ public final class UiNotifications { } try { - System.out.println(raw); var reader = new JsonReader(new java.io.StringReader(raw)); reader.setStrictness(Strictness.LENIENT); @@ -42,7 +41,6 @@ public final class UiNotifications { } } catch (Exception ignored) { - System.out.println(ignored.getMessage()); // fallback to raw text } @@ -54,7 +52,7 @@ public final class UiNotifications { return; } currentText = text; - visibleUntilMs = System.currentTimeMillis() + DEFAULT_DURATION_MS; + remainingTicks = DEFAULT_DURATION_TICKS; } public synchronized void showMessage(String message) { @@ -65,10 +63,7 @@ public final class UiNotifications { } public synchronized void render(DrawContext context, TextRenderer textRenderer, int width, int height) { - if (currentText == null || currentText.getString().isBlank()) { - return; - } - if (System.currentTimeMillis() > visibleUntilMs) { + if (!isVisible()) { return; } @@ -83,5 +78,20 @@ public final class UiNotifications { context.fill(x, y, x + boxWidth, y + boxHeight, Color.GRAY.getRGB()); context.drawTextWithShadow(textRenderer, currentText, x + padding, y + padding, Color.WHITE.getRGB()); } + + public synchronized void tick() { + if (remainingTicks <= 0) { + return; + } + remainingTicks--; + if (remainingTicks <= 0) { + currentText = Text.empty(); + remainingTicks = 0; + } + } + + public synchronized boolean isVisible() { + return currentText != null && !currentText.getString().isBlank() && remainingTicks > 0; + } } diff --git a/src/client/resources/assets/spmega/lang/en_us.json b/src/client/resources/assets/spmega/lang/en_us.json index 3d2e77f..ab60fd2 100644 --- a/src/client/resources/assets/spmega/lang/en_us.json +++ b/src/client/resources/assets/spmega/lang/en_us.json @@ -1,5 +1,19 @@ { "category.spmega": "SPMega", - "key.spmega.open_menu": "Open SPMega Menu" + "key.spmega.open_menu": "Open SPMega Menu", + "key.spmega.scan_qr": "Scan QR from Screen", + "button.spmega.scan_qr": "Scan QR", + "button.spmega.qr.cancel": "Cancel", + "button.spmega.qr.open_link": "Open Link", + "screen.spmega.qr.confirm_title": "Link confirmation", + "screen.spmega.qr.accept_link": "Open this link?", + "message.spmega.qr.capture_failed": "Failed to capture Minecraft screen", + "message.spmega.qr.not_found": "QR code not found on screen", + "message.spmega.qr.invalid_url": "QR does not contain a valid http/https URL", + "message.spmega.qr.opened": "Opened: %s", + "message.spmega.qr.error": "Failed to scan QR from screen", + "message.spmega.qr.hover_tip": "Click to open link", + "message.spmega.qr.found_link": "Found link: %s", + "message.spmega.qr.failed_open": "Failed to open link" } diff --git a/src/main/java/git/yawaflua/tech/spmega/ModConfig.java b/src/main/java/git/yawaflua/tech/spmega/ModConfig.java index 075e3f3..338e7d0 100644 --- a/src/main/java/git/yawaflua/tech/spmega/ModConfig.java +++ b/src/main/java/git/yawaflua/tech/spmega/ModConfig.java @@ -1,7 +1,7 @@ package git.yawaflua.tech.spmega; public record ModConfig(String apiDomain, String apiToken, boolean signQuickPayEnabled) { - public static final String DEFAULT_API_DOMAIN = "https://spworlds.ru"; + public static final String DEFAULT_API_DOMAIN = "https://spmega-api.yawaflua.tech"; public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3"; public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true; diff --git a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java index c68c4e8..af6a208 100644 --- a/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java +++ b/src/main/java/git/yawaflua/tech/spmega/api/SPWorldsApiClient.java @@ -51,52 +51,72 @@ public final class SPWorldsApiClient { public CardInfo getCardInfo(CardAuth auth) throws IOException, InterruptedException { HttpRequest request = requestBuilder("/api/public/card", auth).GET().build(); String body = send(request); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L; - String webhook = json.has("webhook") && !json.get("webhook").isJsonNull() - ? json.get("webhook").getAsString() - : ""; - return new CardInfo(balance, webhook); + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L; + String webhook = json.has("webhook") && !json.get("webhook").isJsonNull() + ? json.get("webhook").getAsString() + : ""; + return new CardInfo(balance, webhook); + } catch (Exception e) { + System.out.println("Failed to parse card info response: " + e.getMessage()); + System.out.println(body); + + throw new IOException("Failed to parse card info response", e); + } } public List getPlayerCards(String username, CardAuth auth) throws IOException, InterruptedException { HttpRequest request = requestBuilder("/api/public/accounts/" + username + "/cards", auth).GET().build(); String body = send(request); - JsonArray json = JsonParser.parseString(body).getAsJsonArray(); + try { + JsonArray json = JsonParser.parseString(body).getAsJsonArray(); - List cards = new ArrayList<>(); - for (JsonElement element : json) { - JsonObject card = element.getAsJsonObject(); - String name = card.has("name") ? card.get("name").getAsString() : ""; - String number = card.has("number") ? card.get("number").getAsString() : ""; - cards.add(new PlayerCard(name, number)); + List cards = new ArrayList<>(); + for (JsonElement element : json) { + JsonObject card = element.getAsJsonObject(); + String name = card.has("name") ? card.get("name").getAsString() : ""; + String number = card.has("number") ? card.get("number").getAsString() : ""; + cards.add(new PlayerCard(name, number)); + } + return cards; + } catch (Exception e) { + System.out.println("Failed to parse player cards response: " + e.getMessage()); + System.out.println(body); + throw new IOException("Failed to parse player cards response", e); } - return cards; } public AccountMe getAccountMe(CardAuth auth) throws IOException, InterruptedException { HttpRequest request = requestBuilder("/api/public/accounts/me", auth).GET().build(); String body = send(request); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - String id = json.has("id") ? json.get("id").getAsString() : ""; - String username = json.has("username") ? json.get("username").getAsString() : ""; - String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : ""; + String id = json.has("id") ? json.get("id").getAsString() : ""; + String username = json.has("username") ? json.get("username").getAsString() : ""; + String minecraftUuid = json.has("minecraftUUID") ? json.get("minecraftUUID").getAsString() : ""; - List cards = new ArrayList<>(); - if (json.has("cards") && json.get("cards").isJsonArray()) { - for (JsonElement element : json.getAsJsonArray("cards")) { - JsonObject card = element.getAsJsonObject(); - cards.add(new AccountCard( - getString(card, "id"), - getString(card, "name"), - getString(card, "number"), - card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0 - )); + List cards = new ArrayList<>(); + if (json.has("cards") && json.get("cards").isJsonArray()) { + for (JsonElement element : json.getAsJsonArray("cards")) { + JsonObject card = element.getAsJsonObject(); + cards.add(new AccountCard( + getString(card, "id"), + getString(card, "name"), + getString(card, "number"), + card.has("color") && !card.get("color").isJsonNull() ? card.get("color").getAsInt() : 0 + )); + } } - } - return new AccountMe(id, username, minecraftUuid, cards); + return new AccountMe(id, username, minecraftUuid, cards); + } catch (Exception e) { + System.out.println("Failed to parse account info response: " + e.getMessage()); + System.out.println(body); + + throw new IOException("Failed to parse account info response", e); + } } public TransactionResult createTransaction(CardAuth auth, String receiver, long amount, String comment) @@ -111,9 +131,17 @@ public final class SPWorldsApiClient { .build(); String body = send(request); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L; - return new TransactionResult(balance); + try { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + + long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L; + return new TransactionResult(balance); + } catch (Exception exception) { + System.out.println("Failed to parse transaction response: " + exception.getMessage()); + System.out.println(body); + + throw new IOException("Failed to parse transaction response", exception); + } } private HttpRequest.Builder requestBuilder(String path, CardAuth auth) {