diff --git a/src/main/java/me/cortex/voxy/client/VoxyMiaSetupScreen.java b/src/main/java/me/cortex/voxy/client/VoxyMiaSetupScreen.java new file mode 100644 index 0000000000..57b79f374a --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/VoxyMiaSetupScreen.java @@ -0,0 +1,366 @@ +package me.cortex.voxy.client; + +import me.cortex.voxy.client.config.VoxyConfig; +import me.cortex.voxy.common.Logger; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.components.MultiLineTextWidget; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import com.mojang.blaze3d.Blaze3D; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.concurrent.CompletableFuture; + +public class VoxyMiaSetupScreen extends Screen { + public static final URI MODPACK_GUIDE_URI = URI.create( + "https://outline.mineinabyss.com/s/home/doc/modpack-VbxUNQ4ztj#h-increasing-your-render-distance" + ); + + public static final Component TITLE = Component.translatable("voxy_mia.modpack_setup.title").withStyle(ChatFormatting.BOLD); + public static final Component MESSAGE = Component.translatable("voxy_mia.modpack_setup.message"); + public static final Component STATUS = Component.translatable("voxy_mia.modpack_setup.status").withStyle(ChatFormatting.GRAY); + public static final Component CHECK_DONT_SHOW = Component.translatable("multiplayerWarning.check").withStyle(ChatFormatting.GRAY); + public static final Component ACTION = Component.translatable("voxy_mia.modpack_setup.action"); + + private static final int EARLY_CLOSE_TICKS = 40; // if setup completed within 2 seconds of popup open, immediately close + + protected final LinearLayout layout = LinearLayout.vertical().spacing(8); + protected final LinearLayout statusGroup = LinearLayout.vertical().spacing(2); + protected final LinearLayout footer = LinearLayout.vertical().spacing(4); + + protected final Checkbox dontShowAgainCheck; + protected Button yesButton; + protected Button noButton; + + private int ticksOpen = 0; + private int ticksDelay = 0; + private boolean lastWindowActiveState = true; + private boolean isCheckingLods = false; + + protected boolean guideOpened = false; + protected boolean setupComplete = false; + + protected final Runnable onScreenClosed; + + public VoxyMiaSetupScreen(final Runnable onScreenClosed) { + this(checkLodsSizeAsync(), onScreenClosed); + } + + public VoxyMiaSetupScreen(final CompletableFuture setupCompleteFuture, final Runnable onScreenClosed) { + super(TITLE); + this.onScreenClosed = onScreenClosed; + + this.dontShowAgainCheck = Checkbox.builder(CHECK_DONT_SHOW, this.font).build(); + + if (setupCompleteFuture != null) { + setupCompleteFuture.thenAcceptAsync(complete -> { + if (!complete) return; + this.setupComplete = true; + + if (Minecraft.getInstance().gui.screen() != this) return; // screen not open yet, do not do anything + + if (this.ticksOpen < EARLY_CLOSE_TICKS) { + this.onClose(); // close popup if lod check completed within 2 sec + } else { + this.rebuildWidgets(); // update ui status to completed + } + }, Minecraft.getInstance()); + } + } + + public static boolean shouldShow() { + return !VoxyConfig.CONFIG.skipMiaSetupWarning; + } + + public void openGuide() { + Blaze3D.openUri(MODPACK_GUIDE_URI); + if (!guideOpened) { + guideOpened = true; + this.rebuildWidgets(); // update ui status + } + } + + public static CompletableFuture checkLodsSizeAsync() { + long targetBytes = 1_500_000_000L; // 1.5GB + return readLodsSizeAsync(targetBytes) + .thenApply(result -> result >= targetBytes); + } + + public static CompletableFuture readLodsSizeAsync(long targetThreshold) { + long startTime = System.currentTimeMillis(); + + return CompletableFuture.supplyAsync(() -> { + Path basePath = Minecraft.getInstance().gameDirectory.toPath().resolve(".voxy").resolve("saves"); + + if (!Files.exists(basePath) || !Files.isDirectory(basePath)) return 0L; + + long[] totalBytes = { 0L }; + + try (DirectoryStream stream = Files.newDirectoryStream(basePath, Files::isDirectory)) { + for (Path serverFolder : stream) { + String folderName = serverFolder.getFileName().toString().toLowerCase(); + + // "mineinabyss.com" or any subdomains + if (folderName.equals("mineinabyss.com") || folderName.endsWith(".mineinabyss.com")) { + Files.walkFileTree(serverFolder, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (file.getFileName().toString().endsWith(".sst")) { + totalBytes[0] += attrs.size(); + + if (totalBytes[0] >= targetThreshold) return FileVisitResult.TERMINATE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + + if (totalBytes[0] >= targetThreshold) break; + } + } + } catch (IOException e) { + Logger.error("Error reading voxy LOD file size", e); + } + + return totalBytes[0]; + }) + .whenComplete((bytes, throwable) -> { + long elapsedTime = System.currentTimeMillis() - startTime; + if (throwable != null) { + Logger.error("LOD size scan failed after " + elapsedTime + " ms", throwable); + } else { + Logger.info("Scanned LOD files size: " + formatBytes(bytes) + " in " + elapsedTime + "ms"); + } + }); + } + + private static String formatBytes(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + char pre = "KMGTPE".charAt(exp - 1); + return String.format(java.util.Locale.ROOT, "%.2f %cB", bytes / Math.pow(1024, exp), pre); + } + + protected void onAction(boolean result) { + if (result) { + openGuide(); + return; + } + + this.onClose(); + } + + @Override + public void onClose() { + if (this.dontShowAgainCheck.selected()) { + VoxyConfig.CONFIG.skipMiaSetupWarning = true; + VoxyConfig.CONFIG.save(); + } + + onScreenClosed.run(); + } + + @Override + protected void init() { + if (this.setupComplete && this.ticksOpen < EARLY_CLOSE_TICKS) { + this.onClose(); // close popup if lod check completed within 2 sec + return; + } + + super.init(); + this.layout.removeChildren(); + this.statusGroup.removeChildren(); + this.footer.removeChildren(); + + this.layout.defaultCellSetting().alignHorizontallyCenter(); + this.layout.addChild(new StringWidget(this.title, this.font), s -> s.paddingBottom(8)); + this.layout.addChild(this.addMessage()); + + this.layout.addChild(this.statusGroup, s -> s.paddingVertical(5)); + this.statusGroup.defaultCellSetting().alignHorizontallyCenter(); + this.statusGroup.addChild(new StringWidget(STATUS, this.font)); + this.statusGroup.addChild(new StringWidget(this.getStatusMessage().withStyle(ChatFormatting.BOLD), this.font)); + + this.layout.addChild(dontShowAgainCheck); + + this.layout.addChild(this.footer, s -> s.paddingTop(14)); + this.footer.defaultCellSetting().alignHorizontallyCenter(); + + if (!setupComplete) + this.footer.addChild(new StringWidget(ACTION, this.font)); + + LinearLayout buttonLayout = this.footer.addChild(LinearLayout.horizontal().spacing(4)); + this.addButtons(buttonLayout); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + + if (!guideOpened) + setDelay(100); + } + + protected MutableComponent getStatusMessage() { + if (setupComplete) + return Component.translatable("voxy_mia.modpack_setup.status.completed") + .withStyle(ChatFormatting.GREEN); + + if (!guideOpened) + return Component.translatable("voxy_mia.modpack_setup.status.not_started") + .withStyle(ChatFormatting.RED); + + return Component.translatable("voxy_mia.modpack_setup.status.incomplete") + .withStyle(ChatFormatting.YELLOW); + } + + @Override + public void extractRenderState(final GuiGraphicsExtractor graphics, int mouseX, int mouseY, final float a) { + int borderGray = 0xFF8B8B8B; + int bgDark = 0x80000000; + + // Status box + { + int px = 12; int py = 2; + int x1 = this.statusGroup.getX() - px; + int y1 = this.statusGroup.getY() - py; + int x2 = this.statusGroup.getX() + this.statusGroup.getWidth() + px; + int y2 = this.statusGroup.getY() + this.statusGroup.getHeight() + py; + + graphics.fill(x1, y1, x2, y2, bgDark); + + // 1px outline + graphics.fill(x1 - 1, y1 - 1, x2 + 1, y1, borderGray); + graphics.fill(x1 - 1, y2, x2 + 1, y2 + 1, borderGray); + graphics.fill(x1 - 1, y1, x1, y2, borderGray); + graphics.fill(x2, y1, x2 + 1, y2, borderGray); + } + + // Footer separator + { + int lineWidth = (int) Math.min(200, this.width * 0.8); + int x = (this.width - lineWidth) / 2; + int y = this.footer.getY() - 8; + + graphics.fill(x, y, x + lineWidth, y + 1, borderGray); + } + + super.extractRenderState(graphics, mouseX, mouseY, a); + } + + @Override + protected void repositionElements() { + this.layout.arrangeElements(); + FrameLayout.centerInRectangle(this.layout, this.getRectangle()); + } + + protected LayoutElement addMessage() { + return new MultiLineTextWidget(MESSAGE, this.font) + .setMaxWidth(this.width - 50) + .setCentered(true); + } + + protected void addButtons(final LinearLayout buttonLayout) { + if (setupComplete) { + buttonLayout.addChild(Button.builder(CommonComponents.GUI_DONE, _ -> this.onClose()).build()); + return; + } + + int buttonWidth = 120; + int buttonSpacing = 4; + + this.yesButton = buttonLayout.addChild( + Button.builder(CommonComponents.GUI_YES, _ -> onAction(true)) + .width(buttonWidth) + .build() + ); + this.noButton = buttonLayout.addChild( + Button.builder(CommonComponents.GUI_NO, _ -> onAction(false)) + .width(buttonWidth) + .build() + ); + + this.footer.addChild( + Button.builder( + Component.translatable("voxy_mia.modpack_setup.open_folder"), + _ -> Blaze3D.openPath(Minecraft.getInstance().gameDirectory.toPath()) + ) + .width(buttonWidth*2 + buttonSpacing) + .build() + ); + } + + public void setDelay(final int delay) { + this.ticksDelay = delay; + + if (this.yesButton == null || this.noButton == null) return; + this.yesButton.active = false; + this.noButton.active = false; + } + + @Override + public void tick() { + super.tick(); + + if (this.ticksDelay-- == 0 && this.yesButton != null && this.noButton != null) { + this.yesButton.active = true; + this.noButton.active = true; + } + + checkSetupCompleteOnFocus(); + this.ticksOpen++; + } + + private void checkSetupCompleteOnFocus() { + boolean currentActive = Minecraft.getInstance().isWindowActive(); + + // focus regained + if (currentActive && !this.lastWindowActiveState && !this.isCheckingLods) { + this.isCheckingLods = true; + checkLodsSizeAsync().thenAcceptAsync(isComplete -> { + this.isCheckingLods = false; + + if (isComplete) { + this.setupComplete = true; + this.rebuildWidgets(); // update ui status to completed + } + }, Minecraft.getInstance()); + } + + this.lastWindowActiveState = currentActive; + } + + @Override + public boolean keyPressed(final KeyEvent event) { + if (this.ticksDelay <= 0 && event.isEscape()) { + onAction(false); + return true; + } else { + return super.keyPressed(event); + } + } + + @Override + public boolean shouldCloseOnEsc() { return false; } + + @Override + public Component getNarrationMessage() { + return CommonComponents.joinForNarration(super.getNarrationMessage(), MESSAGE, ACTION); + } +} \ No newline at end of file diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index 9c57c80da5..2400d3aa0c 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -39,6 +39,8 @@ public class VoxyConfig { //False colours voxy's depth attachment instead of compositing it, see blit_texture_depth_cutout.frag public boolean depthDebugView = false; + public boolean skipMiaSetupWarning = false; + public SSAO.SSAOMode getSSAOMode() { var DEFAULT = SSAO.SSAOMode.AUTO; if (this.ssaoMode == null) return DEFAULT; diff --git a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinGui.java b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinGui.java new file mode 100644 index 0000000000..f9b9530ca5 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinGui.java @@ -0,0 +1,22 @@ +package me.cortex.voxy.client.mixin.minecraft; + +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.screens.Screen; +import me.cortex.voxy.client.VoxyMiaSetupScreen; + +import java.util.List; +import java.util.function.Function; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Gui.class) +public class MixinGui { + @Inject(method = "addInitialScreens", at = @At("TAIL")) + public void voxy_mia$addSetupScreen(List> screens, CallbackInfoReturnable cir) { + if (!VoxyMiaSetupScreen.shouldShow()) return; + + screens.add(VoxyMiaSetupScreen::new); + } +} diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index 143bcf5354..6d9d4f8aaf 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -1,4 +1,13 @@ { + "voxy_mia.modpack_setup.title": "Mine in Abyss Modpack §cSetup §lREQUIRED!", + "voxy_mia.modpack_setup.message": "To see the entire abyss at once, you §omust§r complete the setup.\n\nDownload the voxy LODs and read all of the steps in the guide.", + "voxy_mia.modpack_setup.status": "Current status:", + "voxy_mia.modpack_setup.status.not_started": "Setup guide not opened", + "voxy_mia.modpack_setup.status.incomplete": "Setup not completed", + "voxy_mia.modpack_setup.status.completed": "Setup completed successfully", + "voxy_mia.modpack_setup.action": "Open setup guide in browser?", + "voxy_mia.modpack_setup.open_folder": "Open Modpack Folder", + "voxy.config.title": "Voxy", "voxy.config.general": "General", diff --git a/src/main/resources/client.voxy.mixins.json b/src/main/resources/client.voxy.mixins.json index 692bb95e7a..437ff7d728 100644 --- a/src/main/resources/client.voxy.mixins.json +++ b/src/main/resources/client.voxy.mixins.json @@ -20,6 +20,7 @@ "minecraft.MixinClientLevel", "minecraft.MixinDebugScreenEntryList", "minecraft.MixinFogRenderer", + "minecraft.MixinGui", "minecraft.MixinLevelExtractor", "minecraft.MixinLevelRenderer", "minecraft.MixinRenderSystem",