From f23f691fea5c71536817cddec78b110d40a0aea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:43:49 +0800 Subject: [PATCH] Fix file dialog native leak, add new util --- .../commands/commands/SaveMapCommand.java | 23 +---- .../gui/DefaultSettingsWidgetFactory.java | 27 +++--- .../gui/screens/ProxiesScreen.java | 23 +---- .../gui/tabs/builtin/ProfilesTab.java | 38 ++------- .../meteorclient/settings/FileSetting.java | 22 ++--- .../systems/modules/misc/BookBot.java | 2 +- .../meteorclient/utils/misc/FileDialogs.java | 83 +++++++++++++++++++ 7 files changed, 112 insertions(+), 106 deletions(-) create mode 100644 src/main/java/meteordevelopment/meteorclient/utils/misc/FileDialogs.java diff --git a/src/main/java/meteordevelopment/meteorclient/commands/commands/SaveMapCommand.java b/src/main/java/meteordevelopment/meteorclient/commands/commands/SaveMapCommand.java index 4f7956ca55e..9285f4e36c0 100644 --- a/src/main/java/meteordevelopment/meteorclient/commands/commands/SaveMapCommand.java +++ b/src/main/java/meteordevelopment/meteorclient/commands/commands/SaveMapCommand.java @@ -12,6 +12,7 @@ import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.commands.Command; import meteordevelopment.meteorclient.mixin.MapTextureManagerAccessor; +import meteordevelopment.meteorclient.utils.misc.FileDialogs; import net.minecraft.client.multiplayer.ClientSuggestionProvider; import net.minecraft.client.resources.MapTextureManager; import net.minecraft.core.component.DataComponents; @@ -21,10 +22,6 @@ import net.minecraft.world.item.MapItem; import net.minecraft.world.level.saveddata.maps.MapItemSavedData; import org.jspecify.annotations.Nullable; -import org.lwjgl.sdl.SDLDialog; -import org.lwjgl.sdl.SDL_DialogFileFilter; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -35,14 +32,8 @@ public class SaveMapCommand extends Command { private static final SimpleCommandExceptionType MAP_NOT_FOUND = new SimpleCommandExceptionType(Component.literal("You must be holding a filled map.")); private static final SimpleCommandExceptionType OOPS = new SimpleCommandExceptionType(Component.literal("Something went wrong.")); - private final SDL_DialogFileFilter.Buffer filters; - public SaveMapCommand() { super("save-map", "Saves a map to an image.", "sm"); - - MemoryStack stack = MemoryStack.stackPush(); - filters = SDL_DialogFileFilter.malloc(1, stack); - filters.name(stack.UTF8("NBT Files")).pattern(stack.UTF8("nbt")); } @Override @@ -64,14 +55,8 @@ private void saveMap(int scale) throws CommandSyntaxException { MapItemSavedData state = getMapState(); if (map == null || state == null) throw MAP_NOT_FOUND.create(); - SDLDialog.SDL_ShowSaveFileDialog((_, file, _) -> { - if (file == 0) return; - - long filePointer = MemoryUtil.memGetAddress(file); - if (filePointer == 0) return; - - String str = MemoryUtil.memUTF8(filePointer); - if (str.isBlank()) return; + FileDialogs.saveFile("png", selectedFile -> { + String str = selectedFile.getPath(); if (!str.endsWith(".png")) str += ".png"; File path = new File(str); @@ -95,7 +80,7 @@ private void saveMap(int scale) throws CommandSyntaxException { error("Error writing map texture"); MeteorClient.LOG.error(e.toString()); } - }, 0, 0, filters, ""); + }); } private @Nullable MapItemSavedData getMapState() { diff --git a/src/main/java/meteordevelopment/meteorclient/gui/DefaultSettingsWidgetFactory.java b/src/main/java/meteordevelopment/meteorclient/gui/DefaultSettingsWidgetFactory.java index 4e116638bd4..33a4f5f5ab6 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/DefaultSettingsWidgetFactory.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/DefaultSettingsWidgetFactory.java @@ -24,14 +24,12 @@ import meteordevelopment.meteorclient.settings.*; import meteordevelopment.meteorclient.systems.hud.elements.keyboard.KeyboardHud; import meteordevelopment.meteorclient.utils.Utils; +import meteordevelopment.meteorclient.utils.misc.FileDialogs; import meteordevelopment.meteorclient.utils.render.DisplayItemUtils; import meteordevelopment.meteorclient.utils.render.color.SettingColor; import net.minecraft.client.resources.language.I18n; import org.apache.commons.lang3.Strings; -import org.lwjgl.sdl.SDLDialog; -import org.lwjgl.system.MemoryUtil; -import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -496,23 +494,18 @@ private void fileW(WTable table, FileSetting setting) { WButton selectFile = list.add(theme.button("Select File")).widget(); - WLabel fileName = list.add(theme.label((setting.get() != null && setting.get().exists()) ? setting.get().getName() : "No file selected.")).widget(); + WLabel fileName = list.add(theme.label(fileName(setting))).widget(); - selectFile.action = () -> SDLDialog.SDL_ShowOpenFileDialog((_, file, _) -> { - if (file == 0) return; - - long filePointer = MemoryUtil.memGetAddress(file); - if (filePointer == 0) return; - - String path = MemoryUtil.memUTF8(filePointer); - if (!path.isEmpty()) { - setting.set(new File(path)); - fileName.set(setting.get().getName()); - } + selectFile.action = () -> FileDialogs.openFile(setting.filter, file -> { + setting.set(file); + fileName.set(fileName(setting)); + }); - reset(table, setting, () -> fileName.set((setting.get() != null && setting.get().exists()) ? setting.get().getName() : "No file selected.")); - }, 0L, 0, setting.filters, "", false); + reset(table, setting, () -> fileName.set(fileName(setting))); + } + private static String fileName(FileSetting setting) { + return setting.get() != null && setting.get().exists() ? setting.get().getName() : "No file selected."; } // Other diff --git a/src/main/java/meteordevelopment/meteorclient/gui/screens/ProxiesScreen.java b/src/main/java/meteordevelopment/meteorclient/gui/screens/ProxiesScreen.java index 3f8518e4e9e..ea40961ab6b 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/screens/ProxiesScreen.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/screens/ProxiesScreen.java @@ -19,14 +19,10 @@ import meteordevelopment.meteorclient.settings.Settings; import meteordevelopment.meteorclient.systems.proxies.Proxies; import meteordevelopment.meteorclient.systems.proxies.Proxy; +import meteordevelopment.meteorclient.utils.misc.FileDialogs; import meteordevelopment.meteorclient.utils.misc.NbtUtils; import meteordevelopment.meteorclient.utils.network.MeteorExecutor; -import org.lwjgl.sdl.SDLDialog; -import org.lwjgl.sdl.SDL_DialogFileFilter; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,23 +55,8 @@ public void initWidgets() { newBtn.action = () -> mc.gui.setScreen(new EditProxyScreen(theme, null, this::reload)); // Import - MemoryStack stack = MemoryStack.stackPush(); - SDL_DialogFileFilter.Buffer filters = SDL_DialogFileFilter.malloc(1, stack); - filters.name(stack.UTF8("Txt Files")).pattern(stack.UTF8("txt")); - WButton importBtn = l.add(theme.button("Import")).expandX().widget(); - importBtn.action = () -> SDLDialog.SDL_ShowOpenFileDialog((_, fileptr, _) -> { - if (fileptr == 0) return; - - long filePointer = MemoryUtil.memGetAddress(fileptr); - if (filePointer == 0) return; - - String selectedFile = MemoryUtil.memUTF8(filePointer); - if (!selectedFile.isEmpty()){ - File file = new File(selectedFile); - mc.gui.setScreen(new ProxiesImportScreen(theme, file)); - } - }, 0L, 0, filters, "", false); + importBtn.action = () -> FileDialogs.openFile("txt", file -> mc.gui.setScreen(new ProxiesImportScreen(theme, file))); l.add(refreshButton).expandX(); refreshButton.action = () -> Proxies.get().checkProxies(true); diff --git a/src/main/java/meteordevelopment/meteorclient/gui/tabs/builtin/ProfilesTab.java b/src/main/java/meteordevelopment/meteorclient/gui/tabs/builtin/ProfilesTab.java index de8ac3e8d69..81f82cb1e81 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/tabs/builtin/ProfilesTab.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/tabs/builtin/ProfilesTab.java @@ -23,17 +23,13 @@ import meteordevelopment.meteorclient.systems.profiles.Profile; import meteordevelopment.meteorclient.systems.profiles.Profiles; import meteordevelopment.meteorclient.utils.Utils; +import meteordevelopment.meteorclient.utils.misc.FileDialogs; import meteordevelopment.meteorclient.utils.misc.NbtUtils; import meteordevelopment.meteorclient.utils.render.prompts.OkPrompt; import net.minecraft.client.gui.screens.Screen; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtIo; import org.apache.commons.io.FilenameUtils; -import org.lwjgl.sdl.SDLDialog; -import org.lwjgl.sdl.SDLError; -import org.lwjgl.sdl.SDL_DialogFileFilter; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; import java.io.DataOutputStream; import java.io.File; @@ -47,14 +43,6 @@ import static meteordevelopment.meteorclient.MeteorClient.mc; public class ProfilesTab extends Tab { - private static final SDL_DialogFileFilter.Buffer filters; - - static { - MemoryStack stack = MemoryStack.stackPush(); - filters = SDL_DialogFileFilter.malloc(1, stack); - filters.name(stack.UTF8("NBT Files")).pattern(stack.UTF8("nbt")); - } - public ProfilesTab() { super("Profiles"); } @@ -91,16 +79,8 @@ public void initWidgets() { // Import WButton importBtn = l.add(theme.button("Import")).expandX().widget(); importBtn.tooltip = "Import profile"; - importBtn.action = () -> SDLDialog.SDL_ShowOpenFileDialog((_, file, _) -> { + importBtn.action = () -> FileDialogs.openFile("nbt", profileFile -> { try { - if (file == 0) throw new IOException("Dialog Error: " + SDLError.SDL_GetError()); - - long filePointer = MemoryUtil.memGetAddress(file); - if (filePointer == 0) return; - - String path = MemoryUtil.memUTF8(filePointer); - File profileFile = new File(path); - Profile profile = importProfile(profileFile); if (profile != null) @@ -115,7 +95,7 @@ public void initWidgets() { .dontShowAgainCheckboxVisible(false) .show(); } - }, 0L, 0, filters, "", false); + }); } private void initTable(WTable table) { @@ -295,16 +275,10 @@ public void initWidgets() { add(theme.horizontalSeparator()).expandX(); WButton export = add(theme.button("Export profile")).expandX().widget(); - export.action = () -> SDLDialog.SDL_ShowSaveFileDialog((_, file, _) -> { - if (file == 0) return; - - long filePointer = MemoryUtil.memGetAddress(file); - if (filePointer == 0) return; - - String path = MemoryUtil.memUTF8(filePointer); - exportProfile(path, profile, hud.checked, macros.checked, modules.checked, waypoints.checked); + export.action = () -> FileDialogs.saveFile("nbt", file -> { + exportProfile(file.getPath(), profile, hud.checked, macros.checked, modules.checked, waypoints.checked); onClose(); - }, 0, 0, filters, ""); + }); } private WCheckbox addBool(WContainer container, Setting setting) { diff --git a/src/main/java/meteordevelopment/meteorclient/settings/FileSetting.java b/src/main/java/meteordevelopment/meteorclient/settings/FileSetting.java index c750c9c400c..665947de136 100644 --- a/src/main/java/meteordevelopment/meteorclient/settings/FileSetting.java +++ b/src/main/java/meteordevelopment/meteorclient/settings/FileSetting.java @@ -6,19 +6,18 @@ package meteordevelopment.meteorclient.settings; import net.minecraft.nbt.CompoundTag; -import org.lwjgl.sdl.SDL_DialogFileFilter; -import org.lwjgl.system.MemoryStack; import java.io.File; import java.util.function.Consumer; public class FileSetting extends Setting { - public final SDL_DialogFileFilter.Buffer filters; + /** SDL filter extensions, e.g. {@code "txt"}, {@code "txt;json"} or {@code "*"} */ + public final String filter; - public FileSetting(String name, String description, File defaultValue, Consumer onChanged, Consumer> onModuleActivated, IVisible visible, SDL_DialogFileFilter.Buffer filters) { + public FileSetting(String name, String description, File defaultValue, Consumer onChanged, Consumer> onModuleActivated, IVisible visible, String filter) { super(name, description, defaultValue, onChanged, onModuleActivated, visible); - this.filters = filters; + this.filter = filter; } @Override @@ -51,26 +50,17 @@ protected File load(CompoundTag tag) { } public static class Builder extends SettingBuilder { - private SDL_DialogFileFilter.Buffer filter; + private String filter = "*"; public Builder() { super(null); } - public FileSetting.Builder filter(SDL_DialogFileFilter.Buffer filter) { + public FileSetting.Builder filter(String filter) { this.filter = filter; return this; } - public FileSetting.Builder filter(String... filters) { - MemoryStack stack = MemoryStack.stackPush(); - filter = SDL_DialogFileFilter.malloc(filters.length, stack); - for (int i = 0; i < filters.length; i++) { - filter.get(i).name(stack.UTF8("Filter " + i)).pattern(stack.UTF8(filters[i])); - } - return this; - } - @Override public FileSetting build() { return new FileSetting(name, description, defaultValue, onChanged, onModuleActivated, visible, filter); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/BookBot.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/BookBot.java index 06b9e7986ca..15c8cce6e1f 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/BookBot.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/BookBot.java @@ -47,7 +47,7 @@ public class BookBot extends Module { .name("file") .description("Which file to use.") .defaultValue(DEFAULT_FILE) - .filter("*.txt") + .filter("txt") .visible(() -> mode.get() == Mode.File) .build() ); diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/FileDialogs.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/FileDialogs.java new file mode 100644 index 00000000000..9d7262e1742 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/FileDialogs.java @@ -0,0 +1,83 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.misc; + +import meteordevelopment.meteorclient.MeteorClient; +import org.lwjgl.sdl.SDLDialog; +import org.lwjgl.sdl.SDLError; +import org.lwjgl.sdl.SDL_DialogFileCallback; +import org.lwjgl.sdl.SDL_DialogFileFilter; +import org.lwjgl.system.MemoryUtil; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import static meteordevelopment.meteorclient.MeteorClient.mc; +import static org.lwjgl.system.MemoryUtil.NULL; + +/** + * Native file dialogs. The dialog is asynchronous and its callback runs on SDL's own thread, so the result is handed + * back on the client thread. + */ +public final class FileDialogs { + // SDL reads the filters until it is done with the dialog, so they are built once per extension list and kept. + private static final Map filters = new HashMap<>(); + + // The upcall can not be freed while it is executing, so the previous dialog's callback is released when the next + // dialog opens. Dialogs are modal, so there is only ever one in flight. + private static SDL_DialogFileCallback callback; + + private FileDialogs() {} + + /** + * @param extensions SDL filter extensions, e.g. {@code "txt"}, {@code "txt;json"} or {@code "*"} + * @param onSelected called on the client thread with the picked file + */ + public static void openFile(String extensions, Consumer onSelected) { + SDLDialog.SDL_ShowOpenFileDialog(callback(onSelected), NULL, NULL, get(extensions), (ByteBuffer) null, false); + } + + /** + * @param extensions SDL filter extensions, e.g. {@code "png"}, {@code "txt;json"} or {@code "*"} + * @param onSelected called on the client thread with the picked file + */ + public static void saveFile(String extensions, Consumer onSelected) { + SDLDialog.SDL_ShowSaveFileDialog(callback(onSelected), NULL, NULL, get(extensions), (ByteBuffer) null); + } + + private static SDL_DialogFileCallback callback(Consumer onSelected) { + if (callback != null) callback.free(); + + return callback = new SDL_DialogFileCallback() { + @Override + public void invoke(long userdata, long filelist, int filter) { + if (filelist == NULL) { + MeteorClient.LOG.error("File dialog error: {}", SDLError.SDL_GetError()); + return; + } + + long path = MemoryUtil.memGetAddress(filelist); + if (path == NULL) return; // Canceled + + File file = new File(MemoryUtil.memUTF8(path)); + mc.execute(() -> onSelected.accept(file)); + } + }; + } + + private static SDL_DialogFileFilter.Buffer get(String extensions) { + return filters.computeIfAbsent(extensions, extensions1 -> { + String name = extensions1.equals("*") ? "All files" : "*." + extensions1.replace(";", ", *."); + + return SDL_DialogFileFilter.calloc(1) + .name(MemoryUtil.memUTF8(name)) + .pattern(MemoryUtil.memUTF8(extensions1)); + }); + } +}