From ecfe83a244f50f14f0449c311d6bd036cd92c9fc Mon Sep 17 00:00:00 2001 From: Ollie Date: Sun, 18 Oct 2026 01:35:48 +0100 Subject: [PATCH] Fix the causes of the tick, screen and texture crashes and disable modules that throw instead of crashing --- .../meteorclient/MeteorClient.java | 4 +- .../meteorclient/gui/WidgetScreen.java | 3 +- .../gui/renderer/GuiRenderer.java | 5 ++ .../settings/EntityTypeListSettingScreen.java | 12 +-- .../meteorclient/mixin/MinecraftMixin.java | 2 + .../meteorclient/renderer/Texture.java | 8 +- .../meteorclient/systems/modules/Modules.java | 27 +++++++ .../utils/misc/MeteorEventBus.java | 74 +++++++++++++++++++ 8 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorEventBus.java diff --git a/src/main/java/meteordevelopment/meteorclient/MeteorClient.java b/src/main/java/meteordevelopment/meteorclient/MeteorClient.java index 7cac608e0e9..5ea5f55c93a 100644 --- a/src/main/java/meteordevelopment/meteorclient/MeteorClient.java +++ b/src/main/java/meteordevelopment/meteorclient/MeteorClient.java @@ -26,11 +26,11 @@ import meteordevelopment.meteorclient.utils.PreInit; import meteordevelopment.meteorclient.utils.ReflectInit; import meteordevelopment.meteorclient.utils.Utils; +import meteordevelopment.meteorclient.utils.misc.MeteorEventBus; import meteordevelopment.meteorclient.utils.misc.Version; import meteordevelopment.meteorclient.utils.misc.input.KeyAction; import meteordevelopment.meteorclient.utils.misc.input.KeyBinds; import meteordevelopment.meteorclient.utils.network.OnlinePlayers; -import meteordevelopment.orbit.EventBus; import meteordevelopment.orbit.EventHandler; import meteordevelopment.orbit.IEventBus; import net.fabricmc.api.ClientModInitializer; @@ -58,7 +58,7 @@ public class MeteorClient implements ClientModInitializer { public static MeteorAddon ADDON; public static Minecraft mc; - public static final IEventBus EVENT_BUS = new EventBus(); + public static final IEventBus EVENT_BUS = new MeteorEventBus(); public static final File FOLDER = FabricLoader.getInstance().getGameDir().resolve(MOD_ID).toFile(); public static final Logger LOG; diff --git a/src/main/java/meteordevelopment/meteorclient/gui/WidgetScreen.java b/src/main/java/meteordevelopment/meteorclient/gui/WidgetScreen.java index f38ca464863..5c8077d6f7e 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/WidgetScreen.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/WidgetScreen.java @@ -103,8 +103,9 @@ protected void init() { closed = false; if (firstInit) { - firstInit = false; + clear(); initWidgets(); + firstInit = false; } mc.textInputManager().startTextInput(this); diff --git a/src/main/java/meteordevelopment/meteorclient/gui/renderer/GuiRenderer.java b/src/main/java/meteordevelopment/meteorclient/gui/renderer/GuiRenderer.java index 760e827ad62..63e3e36965d 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/renderer/GuiRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/renderer/GuiRenderer.java @@ -5,6 +5,7 @@ package meteordevelopment.meteorclient.gui.renderer; +import com.mojang.renderpearl.api.textures.FilterMode; import it.unimi.dsi.fastutil.Stack; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import meteordevelopment.meteorclient.MeteorClient; @@ -43,6 +44,8 @@ public class GuiRenderer { public static GuiTexture FAVORITE_NO, FAVORITE_YES; public static GuiTexture COPY, PASTE; + public static Texture EMPTY_SPAWN_EGG; + public GuiTheme theme; private final Renderer2D r = new Renderer2D(false); @@ -79,6 +82,8 @@ public static void init() { PASTE = addTexture(MeteorClient.identifier("textures/icons/gui/paste.png")); TEXTURE = TEXTURE_PACKER.pack(); + + EMPTY_SPAWN_EGG = Texture.readResource("/assets/meteor-client/textures/empty_spawn_egg.png", false, FilterMode.NEAREST); } public void begin(GuiGraphicsExtractor graphics) { diff --git a/src/main/java/meteordevelopment/meteorclient/gui/screens/settings/EntityTypeListSettingScreen.java b/src/main/java/meteordevelopment/meteorclient/gui/screens/settings/EntityTypeListSettingScreen.java index a59e7505c2a..78d1f8e54a5 100644 --- a/src/main/java/meteordevelopment/meteorclient/gui/screens/settings/EntityTypeListSettingScreen.java +++ b/src/main/java/meteordevelopment/meteorclient/gui/screens/settings/EntityTypeListSettingScreen.java @@ -5,9 +5,9 @@ package meteordevelopment.meteorclient.gui.screens.settings; -import com.mojang.renderpearl.api.textures.FilterMode; import meteordevelopment.meteorclient.gui.GuiTheme; import meteordevelopment.meteorclient.gui.WindowScreen; +import meteordevelopment.meteorclient.gui.renderer.GuiRenderer; import meteordevelopment.meteorclient.gui.utils.Cell; import meteordevelopment.meteorclient.gui.widgets.WWidget; import meteordevelopment.meteorclient.gui.widgets.containers.WSection; @@ -15,7 +15,6 @@ import meteordevelopment.meteorclient.gui.widgets.containers.WVerticalList; import meteordevelopment.meteorclient.gui.widgets.input.WTextBox; import meteordevelopment.meteorclient.gui.widgets.pressable.WCheckbox; -import meteordevelopment.meteorclient.renderer.Texture; import meteordevelopment.meteorclient.settings.EntityTypeListSetting; import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.misc.Names; @@ -32,7 +31,6 @@ import java.util.function.Consumer; public class EntityTypeListSettingScreen extends WindowScreen { - private static Texture EMPTY_SPAWN_EGG_TEXTURE; private final EntityTypeListSetting setting; @@ -247,13 +245,7 @@ private void addEntityType(WTable table, WCheckbox tableCheckbox, EntityType } if (stack != null) table.add(theme.item(stack)); - else { - if (EMPTY_SPAWN_EGG_TEXTURE == null) { - EMPTY_SPAWN_EGG_TEXTURE = Texture.readResource("/assets/meteor-client/textures/empty_spawn_egg.png", false, FilterMode.NEAREST); - } - - table.add(theme.texture(32, 32, 0, EMPTY_SPAWN_EGG_TEXTURE)); - } + else table.add(theme.texture(32, 32, 0, GuiRenderer.EMPTY_SPAWN_EGG)); // Name diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/MinecraftMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/MinecraftMixin.java index 30ca66bafe6..cb9a0a25f7e 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixin/MinecraftMixin.java +++ b/src/main/java/meteordevelopment/meteorclient/mixin/MinecraftMixin.java @@ -117,6 +117,8 @@ private void onPreTick(CallbackInfo ci) { startUseItemCalled = false; + Modules.get().updateModuleSubscriptions(); + Profiler.get().push(MeteorClient.MOD_ID + "_pre_update"); MeteorClient.EVENT_BUS.post(TickEvent.Pre.get()); Profiler.get().pop(); diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/Texture.java b/src/main/java/meteordevelopment/meteorclient/renderer/Texture.java index 42a193e31d1..3c8b9222e37 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/Texture.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/Texture.java @@ -19,6 +19,7 @@ import org.lwjgl.system.MemoryUtil; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -67,7 +68,7 @@ public void upload(ByteBuffer buffer) { public static Texture readResource(String path, boolean flipY, FilterMode filter) { try (var in = Texture.class.getResourceAsStream(path)) { - if (in == null) return null; + if (in == null) throw new IllegalStateException("Missing built-in texture: " + path); var data = TextureUtil.readResource(in).rewind(); @@ -78,6 +79,7 @@ public static Texture readResource(String path, boolean flipY, FilterMode filter STBImage.stbi_set_flip_vertically_on_load(flipY); ByteBuffer image = STBImage.stbi_load_from_memory(data, width, height, comp, 4); + if (image == null) throw new IllegalStateException("Failed to decode built-in texture " + path + ": " + STBImage.stbi_failure_reason()); var texture = new Texture(width.get(0), height.get(0), GpuFormat.RGBA8_UNORM, filter, filter); texture.upload(image); @@ -87,8 +89,8 @@ public static Texture readResource(String path, boolean flipY, FilterMode filter return texture; } - } catch (IOException _) { - return null; + } catch (IOException e) { + throw new UncheckedIOException("Failed to read built-in texture: " + path, e); } } } diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java index 99cdcb14e2b..161a441cda1 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java @@ -64,6 +64,7 @@ public class Modules extends System { private final Set active = new ReferenceOpenHashSet<>(); private @Nullable Module moduleToBind; private boolean awaitingKeyRelease = false; + private boolean inGame, modulesSubscribed; public Modules() { super("modules"); @@ -323,6 +324,9 @@ private void onGameJoined(GameJoinedEvent event) { module.onActivate(); } } + + inGame = true; + modulesSubscribed = true; } } @@ -335,6 +339,29 @@ private void onGameLeft(GameLeftEvent event) { module.onDeactivate(); } } + + inGame = false; + modulesSubscribed = false; + } + } + + /** + * Module event handlers expect a world and player, so while either is missing (e.g. the world is still loading) + * their listeners are removed and added back once both exist. Called before each tick. + */ + public void updateModuleSubscriptions() { + synchronized (active) { + boolean canUpdate = Utils.canUpdate(); + if (!inGame || canUpdate == modulesSubscribed) return; + + for (Module module : active) { + if (module.runInMainMenu || !module.autoSubscribe) continue; + + if (canUpdate) MeteorClient.EVENT_BUS.subscribe(module); + else MeteorClient.EVENT_BUS.unsubscribe(module); + } + + modulesSubscribed = canUpdate; } } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorEventBus.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorEventBus.java new file mode 100644 index 00000000000..86ee8edf2f1 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorEventBus.java @@ -0,0 +1,74 @@ +/* + * 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 meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.systems.modules.Modules; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import meteordevelopment.orbit.EventBus; +import meteordevelopment.orbit.ICancellable; +import org.jspecify.annotations.Nullable; + +/** + * Disables and reports a module that throws from an event handler instead of crashing the game. + * Exceptions that don't come from a module are rethrown as usual. + */ +public class MeteorEventBus extends EventBus { + @Override + public T post(T event) { + try { + return super.post(event); + } catch (RuntimeException e) { + if (!disableCrashedModule(e)) throw e; + return event; + } + } + + @Override + public T post(T event) { + try { + return super.post(event); + } catch (RuntimeException e) { + if (!disableCrashedModule(e)) throw e; + return event; + } + } + + private static boolean disableCrashedModule(RuntimeException e) { + Module module = findModule(e); + if (module == null) return false; + + MeteorClient.LOG.error("Module {} threw an exception and was disabled", module.name, e); + + if (module.isActive()) { + try { + module.toggle(); + } catch (RuntimeException deactivateException) { + MeteorClient.LOG.error("Module {} also threw while being disabled", module.name, deactivateException); + } + } + + ChatUtils.errorPrefix("Meteor", "(highlight)%s(default) crashed and was disabled, check the log for details.", module.title); + return true; + } + + private static @Nullable Module findModule(Throwable e) { + Modules modules = Modules.get(); + if (modules == null) return null; + + for (StackTraceElement frame : e.getStackTrace()) { + String className = frame.getClassName(); + + for (Module module : modules.getAll()) { + String moduleClass = module.getClass().getName(); + if (className.equals(moduleClass) || className.startsWith(moduleClass + "$")) return module; + } + } + + return null; + } +}