From 48fd2377e03275f6ac4ebcdc623d412e399a88cc Mon Sep 17 00:00:00 2001 From: pomchom Date: Mon, 17 Jun 2024 13:39:07 -0400 Subject: [PATCH] fix /importfile (finally) --- .../io/github/homchom/recode/game/GameTime.kt | 20 +++ .../io/github/homchom/recode/io/NativeIO.kt | 72 ++++++++ .../commands/impl/item/ImportFileCommand.java | 160 ++++++++++-------- .../homchom/recode/ui/screen/DummyScreen.kt | 19 +++ 4 files changed, 201 insertions(+), 70 deletions(-) create mode 100644 src/main/java/io/github/homchom/recode/io/NativeIO.kt create mode 100644 src/main/java/io/github/homchom/recode/ui/screen/DummyScreen.kt diff --git a/src/main/java/io/github/homchom/recode/game/GameTime.kt b/src/main/java/io/github/homchom/recode/game/GameTime.kt index 002c3c912..f33f540c1 100644 --- a/src/main/java/io/github/homchom/recode/game/GameTime.kt +++ b/src/main/java/io/github/homchom/recode/game/GameTime.kt @@ -1,11 +1,19 @@ +@file:JvmName("GameTime") + package io.github.homchom.recode.game import io.github.homchom.recode.Power +import io.github.homchom.recode.RecodeDispatcher import io.github.homchom.recode.event.listen import io.github.homchom.recode.event.listenEach +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.launch +import java.util.concurrent.CompletionStage /** * The current client tick. @@ -21,6 +29,18 @@ val currentTick get() = TickRecorder.currentTick */ suspend fun waitTicks(ticks: Int) = AfterClientTickEvent.notifications.take(ticks).collect() +/** + * @return A [CompletionStage] of a future that completes after the given number of [ticks]. + * + * @see waitTicks + */ +@Deprecated("Only for use in Java code", + ReplaceWith("launch { waitTicks(ticks) }", "kotlinx.coroutines.launch") +) +@DelicateCoroutinesApi +fun waitTicksAsync(ticks: Int): CompletionStage = + GlobalScope.launch(RecodeDispatcher) { waitTicks(ticks) }.asCompletableFuture() + private object TickRecorder { var currentTick = 0L diff --git a/src/main/java/io/github/homchom/recode/io/NativeIO.kt b/src/main/java/io/github/homchom/recode/io/NativeIO.kt new file mode 100644 index 000000000..77d5f4964 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/io/NativeIO.kt @@ -0,0 +1,72 @@ +@file:JvmName("NativeIO") + +package io.github.homchom.recode.io + +import io.github.homchom.recode.mc +import org.lwjgl.PointerBuffer +import org.lwjgl.system.MemoryStack +import org.lwjgl.util.tinyfd.TinyFileDialogs +import java.io.IOException +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.pathString + +/** + * Opens a native file picker, blocks the current thread until the dialog closes, and returns the [Path] to + * the picked file if one was picked. + * + * @param defaultPath The path to start at. If `null`, the operating system will choose. + * @param filter The [FileFilter] to apply. If `null`, all files will be permitted. + * + * @throws IOException If the native file dialog errors for any reason. + */ +@JvmOverloads +fun pickFile(title: String? = null, defaultPath: Path? = null, filter: FileFilter? = null) = + openFileDialog(filter?.patterns) { filterBuffer -> + TinyFileDialogs.tinyfd_openFileDialog( + title, + defaultPath?.pathString, + filterBuffer, + filter?.description, + false + )?.let(::Path) + } + +/** + * Opens a native file picker, blocks the current thread until the dialog closes, and returns the [Path]s to + * the picked files if any were picked. The opened file picker can pick multiple files. + * + * @see pickFile + */ +@JvmOverloads +fun pickMultipleFiles(title: String? = null, defaultPath: Path? = null, filter: FileFilter? = null) = + openFileDialog(filter?.patterns) { filterBuffer -> + val joinedPaths = TinyFileDialogs.tinyfd_openFileDialog( + title, + defaultPath?.pathString, + filterBuffer, + filter?.description, + true + ) + joinedPaths?.split('|')?.map(::Path) + } + +/** + * A file filter, to be passed to [pickFile] or [pickMultipleFiles]. + */ +data class FileFilter(val patterns: List, val description: String? = null) + +private inline fun openFileDialog( + filterPatterns: List?, + nativeDialogFunction: (PointerBuffer?) -> R? +): R? { + MemoryStack.stackPush().use { stack -> + val filterPatternBuffer = filterPatterns?.let { patterns -> + val buffer = stack.mallocPointer(patterns.size) + for (pattern in patterns) buffer.put(stack.UTF8(pattern)) + buffer.flip() + } + if (mc.isRunning) mc.mouseHandler.releaseMouse() + return nativeDialogFunction(filterPatternBuffer) + } +} \ No newline at end of file diff --git a/src/main/java/io/github/homchom/recode/mod/commands/impl/item/ImportFileCommand.java b/src/main/java/io/github/homchom/recode/mod/commands/impl/item/ImportFileCommand.java index da209a803..9018c1ada 100644 --- a/src/main/java/io/github/homchom/recode/mod/commands/impl/item/ImportFileCommand.java +++ b/src/main/java/io/github/homchom/recode/mod/commands/impl/item/ImportFileCommand.java @@ -4,7 +4,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.brigadier.CommandDispatcher; -import io.github.homchom.recode.LegacyRecode; +import io.github.homchom.recode.ModConstants; +import io.github.homchom.recode.game.GameTime; +import io.github.homchom.recode.io.NativeIO; import io.github.homchom.recode.mod.commands.Command; import io.github.homchom.recode.mod.commands.arguments.ArgBuilder; import io.github.homchom.recode.sys.hypercube.templates.CompressionUtil; @@ -12,21 +14,23 @@ import io.github.homchom.recode.sys.player.chat.ChatType; import io.github.homchom.recode.sys.player.chat.ChatUtil; import io.github.homchom.recode.sys.util.ItemUtil; +import io.github.homchom.recode.ui.screen.DummyScreen; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.kyori.adventure.text.Component; import net.minecraft.client.Minecraft; import net.minecraft.commands.CommandBuildContext; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; -import java.awt.*; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Scanner; +// TODO: clean up further public class ImportFileCommand extends Command { + private final String TITLE = "Pick a file"; @Override public void register(Minecraft mc, CommandDispatcher cd, CommandBuildContext context) { @@ -35,78 +39,94 @@ public void register(Minecraft mc, CommandDispatcher if (!isCreative(mc)) return -1; ChatUtil.sendMessage("Opening File Picker", ChatType.INFO_BLUE); - LegacyRecode.executor.submit(() -> { - try { - FileDialog fd = new FileDialog((Dialog) null, "Choose a text file", FileDialog.LOAD); - fd.setMultipleMode(true); - fd.setVisible(true); - File[] files = fd.getFiles(); - fd.dispose(); - if (files == null || files.length == 0) { - ChatUtil.sendMessage("You didnt choose a file!", ChatType.FAIL); - return; - } - - int valid = 0; - files: - for (File f : files) { - if (files.length != 1) - ChatUtil.sendMessage("Loading file: " + f.getName(), ChatType.INFO_BLUE); - Scanner sc = new Scanner(f, StandardCharsets.UTF_8); - - List lines = new ArrayList<>(); - - while (sc.hasNextLine()) { - String line = sc.nextLine(); - if (line.length() > 2000) { - ChatUtil.sendMessage("Line " + (lines.size() + 1) + " is too long! (" + line.length() + " > 2000)", ChatType.FAIL); - continue files; - } - lines.add(line); - if (lines.size() > 10000) { - ChatUtil.sendMessage("File contains contains too many lines! (Max: 10000)", ChatType.FAIL); - continue files; - } - } - - List blocks = new ArrayList<>(); - List current = new ArrayList<>(); - - boolean first = true; - for (String line : lines) { - current.add(line); - if (current.size() >= 26) { - blocks.add(block(current, first)); - first = false; - current = new ArrayList<>(); - } - } - if (!current.isEmpty()) blocks.add(block(current, first)); - - String template = template(blocks); - if (template.getBytes().length > 65536) {//i have no idea what the actual limit is it just seems to be close to this - ChatUtil.sendMessage("Your file is too large!", ChatType.FAIL); - } else { - ItemStack item = new ItemStack(Items.ENDER_CHEST); - TemplateUtil.applyRawTemplateNBT(item, f.getName(), "CodeUtilities", template); - ItemUtil.giveCreativeItem(item, files.length == 1); - if (files.length != 1) Thread.sleep(500); - valid++; - } - } - if (files.length != 1 && valid > 0) - ChatUtil.sendMessage("Loaded " + valid + " files!", ChatType.SUCCESS); - - } catch (Exception err) { - err.printStackTrace(); - ChatUtil.sendMessage("Unexpected Error.", ChatType.FAIL); - } - }); + Minecraft.getInstance().tell(this::openFilePicker); + return 1; }) ); } + private void openFilePicker() { + var screen = new DummyScreen(Component.text(TITLE), true); + Minecraft.getInstance().setScreen(screen); + + GameTime.waitTicksAsync(1).thenRun(() -> { + var paths = NativeIO.pickMultipleFiles(TITLE); + Minecraft.getInstance().setScreen(null); + if (paths == null || paths.isEmpty()) { + ChatUtil.sendMessage("You didnt choose a file!", ChatType.FAIL); + return; + } + + int valid = 0; + files: for (var path : paths) { + if (paths.size() != 1) { + ChatUtil.sendMessage("Loading file: " + path.getFileName(), ChatType.INFO_BLUE); + } + Scanner sc; + try { + sc = new Scanner(path, StandardCharsets.UTF_8); + } catch (IOException e) { + ChatUtil.sendMessage("Failed to load file: " + path.getFileName(), ChatType.FAIL); + continue; + } + + List lines = new ArrayList<>(); + + while (sc.hasNextLine()) { + String line = sc.nextLine(); + if (line.length() > 10000) { + ChatUtil.sendMessage("Line " + (lines.size() + 1) + " is too long! (" + line.length() + " > 10000)", ChatType.FAIL); + continue files; + } + lines.add(line); + if (lines.size() > 10000) { + ChatUtil.sendMessage("File contains contains too many lines! (Max: 10,000)", ChatType.FAIL); + continue files; + } + } + + List blocks = new ArrayList<>(); + List current = new ArrayList<>(); + + boolean first = true; + for (String line : lines) { + current.add(line); + if (current.size() >= 26) { + blocks.add(block(current, first)); + first = false; + current = new ArrayList<>(); + } + } + if (!current.isEmpty()) blocks.add(block(current, first)); + + String template; + try { + template = template(blocks); + } catch (IOException e) { + ChatUtil.sendMessage("Failed to generate template for file: " + path.getFileName(), ChatType.FAIL); + continue; + } + if (template.getBytes().length > 65536) { // i have no idea what the actual limit is it just seems to be close to this TODO: nice + ChatUtil.sendMessage("Your file is too large!", ChatType.FAIL); + } else { + ItemStack item = new ItemStack(Items.ENDER_CHEST); + TemplateUtil.applyRawTemplateNBT(item, path.getFileName().toString(), ModConstants.MOD_NAME, template); + ItemUtil.giveCreativeItem(item, paths.size() == 1); + if (paths.size() != 1) try { + Thread.sleep(500); + } catch (InterruptedException e) { + // temporary TODO: what + throw new RuntimeException(e); + } + valid++; + } + } + if (paths.size() != 1 && valid > 0) + ChatUtil.sendMessage("Loaded " + valid + " files!", ChatType.SUCCESS); + }); + } + private String template(List iblocks) throws IOException { JsonArray blocks = new JsonArray(); blocks.add(JsonParser.parseString("{\"id\":\"block\",\"block\":\"func\",\"args\":{\"items\":[]},\"data\":\"file\"}")); diff --git a/src/main/java/io/github/homchom/recode/ui/screen/DummyScreen.kt b/src/main/java/io/github/homchom/recode/ui/screen/DummyScreen.kt new file mode 100644 index 000000000..af9637030 --- /dev/null +++ b/src/main/java/io/github/homchom/recode/ui/screen/DummyScreen.kt @@ -0,0 +1,19 @@ +package io.github.homchom.recode.ui.screen + +import io.github.homchom.recode.ui.text.toVanilla +import net.kyori.adventure.text.Component +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen + +/** + * A dummy [Screen] whose sole purpose is to release the mouse. This is, for example, useful in conjunction + * with native IO functions like [io.github.homchom.recode.io.pickFile]. + */ +class DummyScreen( + title: Component, + private val renderBackground: Boolean = true +) : Screen(title.toVanilla()) { + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, tickDelta: Float) { + if (renderBackground) renderBackground(guiGraphics, mouseX, mouseY, tickDelta) + } +} \ No newline at end of file