Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 366 additions & 0 deletions src/main/java/me/cortex/voxy/client/VoxyMiaSetupScreen.java
Original file line number Diff line number Diff line change
@@ -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<Boolean> 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<Boolean> checkLodsSizeAsync() {
long targetBytes = 1_500_000_000L; // 1.5GB
return readLodsSizeAsync(targetBytes)
.thenApply(result -> result >= targetBytes);
}

public static CompletableFuture<Long> 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<Path> 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);
}
}
2 changes: 2 additions & 0 deletions src/main/java/me/cortex/voxy/client/config/VoxyConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinGui.java
Original file line number Diff line number Diff line change
@@ -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<Function<Runnable, Screen>> screens, CallbackInfoReturnable<Boolean> cir) {
if (!VoxyMiaSetupScreen.shouldShow()) return;

screens.add(VoxyMiaSetupScreen::new);
}
}
Loading