diff --git a/src/main/java/shit/zen/ZenClient.java b/src/main/java/shit/zen/ZenClient.java index 8304117..dba0eaa 100644 --- a/src/main/java/shit/zen/ZenClient.java +++ b/src/main/java/shit/zen/ZenClient.java @@ -18,6 +18,7 @@ import shit.zen.event.EventBus; import shit.zen.event.EventTarget; import shit.zen.event.impl.TickEvent; import shit.zen.gui.IntroAnimation; +import shit.zen.log.LogCollector; import shit.zen.manager.CommandManager; import shit.zen.manager.ConfigManager; import shit.zen.manager.GroupManager; @@ -27,6 +28,7 @@ import shit.zen.manager.ModuleManager; import shit.zen.manager.TargetManager; import shit.zen.patch.BlockOcclusionCachePatch; import shit.zen.patch.BlockPatch; +import shit.zen.patch.BufferUploaderPatch; import shit.zen.patch.ChatScreenPatch; import shit.zen.patch.ClientLevelPatch; import shit.zen.patch.ConnectionPatch; @@ -48,6 +50,7 @@ import shit.zen.patch.MinecraftPatch; import shit.zen.patch.PacketUtilsPatch; import shit.zen.patch.PlayerPatch; import shit.zen.patch.PlayerTabOverlayPatch; +import shit.zen.patch.ScreenEffectRendererPatch; import shit.zen.asm.Bootstrap; import shit.zen.utils.rotation.RotationHandler; @@ -76,6 +79,7 @@ public class ZenClient extends ClientBase { private HudManager hudManager; private LagManager lagManager; private TargetManager targetManager; + private LogCollector logCollector; private int reconnectAttempts; public ZenClient() { @@ -96,6 +100,9 @@ public class ZenClient extends ClientBase { this.eventBus = new EventBus(); this.rotationHandler = new RotationHandler(); this.eventBus.register(this.rotationHandler); + this.logCollector = new LogCollector(); + this.logCollector.install(); + this.eventBus.register(this.logCollector); this.moduleManager = new ModuleManager(); this.groupManager = new GroupManager(); this.hudManager = new HudManager(); @@ -212,6 +219,8 @@ public class ZenClient extends ClientBase { PatchRegistry.register(ItemInHandLayerPatch.class); PatchRegistry.register(HumanoidModelPatch.class); PatchRegistry.register(LivingEntityRendererPatch.class); + PatchRegistry.register(BufferUploaderPatch.class); + PatchRegistry.register(ScreenEffectRendererPatch.class); PatchRegistry.register(ItemPatch.class); PatchRegistry.register(PlayerTabOverlayPatch.class); PatchRegistry.register(FriendlyByteBufPatch.class); diff --git a/src/main/java/shit/zen/command/impl/LogCommand.java b/src/main/java/shit/zen/command/impl/LogCommand.java new file mode 100644 index 0000000..f01615f --- /dev/null +++ b/src/main/java/shit/zen/command/impl/LogCommand.java @@ -0,0 +1,51 @@ +package shit.zen.command.impl; + +import java.io.File; +import shit.zen.ClientBase; +import shit.zen.command.Command; +import shit.zen.log.LogCollector; +import shit.zen.utils.misc.ChatUtil; + +public class LogCommand extends Command { + public LogCommand() { + super("log", new String[]{"logs"}); + } + + @Override + public void onCommand(String[] args) { + if (args.length != 1) { + ChatUtil.print("Usage: log export/clear/count/folder"); + return; + } + switch (args[0].toLowerCase()) { + case "export" -> { + if (ClientBase.mc != null) { + // FileDialogUtil blocks with a modal dialog; keep it off the netty thread. + ClientBase.mc.execute(() -> LogCollector.INSTANCE.exportAndReport()); + } + } + case "clear" -> { + LogCollector.INSTANCE.clear(); + ChatUtil.print("\u00a7a\u5df2\u6e05\u7a7a\u5df2\u6536\u96c6\u7684\u65e5\u5fd7"); + } + case "count" -> + ChatUtil.print("\u5df2\u6536\u96c6 " + LogCollector.INSTANCE.getCount() + " \u6761\u65e5\u5fd7"); + case "folder" -> { + try { + Runtime.getRuntime().exec("explorer " + + new File(ClientBase.mc.gameDirectory, "logs").getAbsolutePath()); + } catch (Exception ignored) { + } + } + default -> ChatUtil.print("Usage: log export/clear/count/folder"); + } + } + + @Override + public String[] onTab(String[] args) { + if (args.length == 0) { + return new String[]{"export", "clear", "count", "folder"}; + } + return new String[0]; + } +} diff --git a/src/main/java/shit/zen/gui/legacy/CategoryPanel.java b/src/main/java/shit/zen/gui/legacy/CategoryPanel.java index 3ef1188..8c63eb2 100644 --- a/src/main/java/shit/zen/gui/legacy/CategoryPanel.java +++ b/src/main/java/shit/zen/gui/legacy/CategoryPanel.java @@ -8,6 +8,8 @@ import shit.zen.ZenClient; import shit.zen.gui.legacy.ModuleButton; import shit.zen.modules.Category; import shit.zen.modules.Module; +import shit.zen.render.FontPresets; +import shit.zen.render.FontRenderer; import shit.zen.render.FontStore; import shit.zen.utils.render.RenderUtil; @@ -92,10 +94,17 @@ public class CategoryPanel { case EXPLOIT -> 'e'; case WORLD -> 'f'; case MISC -> 'g'; + case XLZEN -> '\uE8B8'; case GROUPS -> 'h'; }); - float iconY = (float)this.y + ((float)this.rowHeight / 2.0f - FontStore.ICON_30.getFontHeight() / 2.0f) + 3.0f; - FontStore.ICON_30.drawStringWithShadow(guiGraphics.pose(), iconChar, this.x + 4, iconY, -1); + if (this.category == Category.XLZEN) { + FontRenderer materialFont = FontPresets.materialIcons(30.0f); + float iconY = (float)this.y + ((float)this.rowHeight / 2.0f - materialFont.getFont().getFontHeight() / 2.0f) + 3.0f; + materialFont.getFont().drawStringWithShadow(guiGraphics.pose(), iconChar, this.x + 4, iconY, -1); + } else { + float iconY = (float)this.y + ((float)this.rowHeight / 2.0f - FontStore.ICON_30.getFontHeight() / 2.0f) + 3.0f; + FontStore.ICON_30.drawStringWithShadow(guiGraphics.pose(), iconChar, this.x + 4, iconY, -1); + } float labelY = (float)this.y + ((float)this.rowHeight / 2.0f - FontStore.OPENSANS_18.getFontHeight() / 2.0f) - 0.5f; FontStore.OPENSANS_18.drawStringWithShadow(guiGraphics.pose(), this.category.displayName, this.x + this.rowHeight + 4, labelY, -1); for (ModuleButton moduleButton : this.moduleButtons) { diff --git a/src/main/java/shit/zen/gui/panel/CategoryBar.java b/src/main/java/shit/zen/gui/panel/CategoryBar.java index 493b3d5..df4e7c6 100644 --- a/src/main/java/shit/zen/gui/panel/CategoryBar.java +++ b/src/main/java/shit/zen/gui/panel/CategoryBar.java @@ -157,6 +157,7 @@ extends ClientBase { CATEGORY_ICONS.put(Category.EXPLOIT, "\uE894"); CATEGORY_ICONS.put(Category.WORLD, "\uE5D3"); CATEGORY_ICONS.put(Category.MISC, "\uE24D"); + CATEGORY_ICONS.put(Category.XLZEN, "\uE8B8"); CATEGORY_ICONS.put(Category.GROUPS, "\uE2C7"); } } \ No newline at end of file diff --git a/src/main/java/shit/zen/hud/TargetHealthHud.java b/src/main/java/shit/zen/hud/TargetHealthHud.java new file mode 100644 index 0000000..90529d2 --- /dev/null +++ b/src/main/java/shit/zen/hud/TargetHealthHud.java @@ -0,0 +1,125 @@ +package shit.zen.hud; + +import java.awt.Color; +import net.minecraft.network.protocol.game.ClientboundDamageEventPacket; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import shit.zen.ClientBase; +import shit.zen.event.impl.Render2DEvent; +import shit.zen.modules.impl.combat.KillAura; +import shit.zen.render.DrawContext; +import shit.zen.render.FontPresets; +import shit.zen.render.FontRenderer; +import shit.zen.render.Paint; +import shit.zen.render.Renderer; +import shit.zen.render.RoundedRectangle; + +/** + * Watermark companion: shows the health of the player currently in combat + * with the user - either the one the user is attacking (KillAura target or + * the victim of the user's hits) or the one attacking the user. Combat + * partners are learned from ClientboundDamageEventPacket broadcasts, so it + * works in multiplayer for both directions. + */ +public class TargetHealthHud extends ClientBase { + private static final long COMBAT_TIMEOUT = 5000L; + private static final long FADE_MS = 800L; + private static final float PAD = 7.0f; + private static final float BAR_HEIGHT = 3.0f; + + private final FontRenderer nameFont = FontPresets.poppinsMedium(12.0f); + private final FontRenderer hpFont = FontPresets.poppinsMedium(10.0f); + + private Player target; + private long lastCombat; + + public void onDamagePacket(ClientboundDamageEventPacket packet) { + if (mc == null || mc.level == null || mc.player == null) { + return; + } + int victimId = packet.entityId(); + int causeId = packet.sourceCauseId(); + if (victimId == mc.player.getId()) { + Entity attacker = mc.level.getEntity(causeId); + if (attacker instanceof Player player && player != mc.player) { + this.note(player); + } + } else if (causeId == mc.player.getId()) { + Entity victim = mc.level.getEntity(victimId); + if (victim instanceof Player player && victim != mc.player) { + this.note(player); + } + } + } + + public void note(Entity entity) { + if (entity instanceof Player player) { + this.target = player; + this.lastCombat = System.currentTimeMillis(); + } + } + + public void render(Render2DEvent event) { + if (mc == null || mc.player == null || mc.level == null) { + return; + } + if (KillAura.aimingTarget instanceof Player auraTarget && auraTarget != mc.player) { + this.note(auraTarget); + } + Player target = this.target; + if (target == null) { + return; + } + long since = System.currentTimeMillis() - this.lastCombat; + if (since > COMBAT_TIMEOUT || !target.isAlive() || target.isRemoved() + || mc.player.distanceToSqr(target) > 6400.0) { + this.target = null; + return; + } + float alpha = since > COMBAT_TIMEOUT - FADE_MS ? (COMBAT_TIMEOUT - since) / (float) FADE_MS : 1.0f; + float hp = Mth.clamp(target.getHealth(), 0.0f, target.getMaxHealth()); + float max = Math.max(1.0f, target.getMaxHealth()); + float ratio = Mth.clamp(hp / max, 0.0f, 1.0f); + String name = target.getName().getString(); + String hpText = String.format("%.1f / %.0f", hp, max); + float nameWidth = this.nameFont.getWidth(name); + float hpWidth = this.hpFont.getWidth(hpText); + float lineHeight = this.nameFont.getMetrics().capHeight(); + float width = PAD * 2.0f + nameWidth + 8.0f + hpWidth; + float height = PAD + lineHeight + 5.0f + BAR_HEIGHT + PAD; + float x = mc.getWindow().getGuiScaledWidth() - width - 8.0f; + float y = 8.0f; + float barY = y + PAD + lineHeight + 5.0f; + float barWidth = width - PAD * 2.0f; + Color fill = Color.getHSBColor(Math.max(0.0f, ratio) / 3.0f, 0.9f, 1.0f); + int bgColor = new Color(0, 0, 0, (int) (140 * alpha)).getRGB(); + int textColor = new Color(255, 255, 255, (int) (255 * alpha)).getRGB(); + int shadowColor = new Color(0, 0, 0, (int) (120 * alpha)).getRGB(); + int barBgColor = new Color(60, 60, 60, (int) (200 * alpha)).getRGB(); + int barColor = new Color(fill.getRed(), fill.getGreen(), fill.getBlue(), (int) (255 * alpha)).getRGB(); + float hpX = x + width - PAD - hpWidth; + float drawX = x; + float drawY = y + PAD; + float textY = drawY; + float barFillWidth = barWidth * ratio; + Renderer.renderConsumer(drawContext -> { + try (Paint paint = new Paint()) { + paint.setColor(bgColor); + drawContext.drawRoundedRect(RoundedRectangle.ofXYWHR(drawX, drawY - PAD + 2.0f, width, height, 4.0f), paint); + paint.setColor(barBgColor); + drawContext.drawRoundedRect(RoundedRectangle.ofXYWHR(drawX + PAD, barY, barWidth, BAR_HEIGHT, BAR_HEIGHT / 2.0f), paint); + if (barFillWidth > 0.5f) { + paint.setColor(barColor); + drawContext.drawRoundedRect(RoundedRectangle.ofXYWHR(drawX + PAD, barY, barFillWidth, BAR_HEIGHT, BAR_HEIGHT / 2.0f), paint); + } + paint.setColor(shadowColor); + drawContext.drawString(name, drawX + PAD + 0.5f, textY + 0.5f, this.nameFont, paint); + drawContext.drawString(hpText, hpX + 0.5f, textY + 1.0f, this.hpFont, paint); + paint.setColor(textColor); + drawContext.drawString(name, drawX + PAD, textY, this.nameFont, paint); + drawContext.drawString(hpText, hpX, textY + 0.5f, this.hpFont, paint); + } + }); + } +} diff --git a/src/main/java/shit/zen/log/LogCollector.java b/src/main/java/shit/zen/log/LogCollector.java new file mode 100644 index 0000000..88d4451 --- /dev/null +++ b/src/main/java/shit/zen/log/LogCollector.java @@ -0,0 +1,356 @@ +package shit.zen.log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundDisconnectPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; +import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Property; +import shit.zen.ClientBase; +import shit.zen.ZenClient; +import shit.zen.event.EventTarget; +import shit.zen.event.impl.PacketEvent; +import shit.zen.event.impl.TickEvent; +import shit.zen.event.impl.WorldChangeEvent; +import shit.zen.utils.misc.ChatUtil; +import shit.zen.utils.misc.FileDialogUtil; + +/** + * Collects every log stream the client can reach and bundles them into one + * exportable archive: + * + * - ALL log4j2 output in real time (Minecraft, Forge, every mod, and the + * client's own "Client"/shit.zen loggers) via a root-logger appender, + * - inbound server traffic that has no log4j footprint: player chat, + * system broadcasts (join/leave/deaths), disconnect reasons, + * - session/environment info, + * and at export time additionally sweeps the on-disk artifacts: + * latest.log, the native injection logs (%TEMP%/openzen-*.log) and recent + * crash reports. + */ +public class LogCollector { + public static LogCollector INSTANCE; + + private static final String APPENDER_NAME = "OpenZenLogCollector"; + private static final int MAX_ENTRIES = 20000; + private static final int MAX_CRASH_REPORTS = 5; + private static final int MAX_STACK_CHARS = 4096; + private static final Object LOCK = new Object(); + // re-entrancy guard: anything logged while capturing must not re-enter + private static final ThreadLocal CAPTURING = ThreadLocal.withInitial(() -> Boolean.FALSE); + + private final ArrayDeque lines = new ArrayDeque<>(); + private volatile boolean installed; + private long sessionStart; + private boolean environmentLogged; + + public LogCollector() { + INSTANCE = this; + } + + private static final class Line { + final long time; + final String source; + final String message; + + Line(long time, String source, String message) { + this.time = time; + this.source = source; + this.message = message; + } + } + + public void install() { + if (this.installed) { + return; + } + this.sessionStart = System.currentTimeMillis(); + try { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration configuration = context.getConfiguration(); + if (configuration.getAppender(APPENDER_NAME) == null) { + AbstractAppender appender = new AbstractAppender(APPENDER_NAME, null, null, true, Property.EMPTY_ARRAY) { + @Override + public void append(LogEvent event) { + LogCollector.this.captureLog(event); + } + }; + appender.start(); + configuration.addAppender(appender); + configuration.getRootLogger().addAppender(appender, Level.ALL, null); + context.updateLoggers(); + } + this.installed = true; + } catch (Throwable ignored) { + // never re-log here or the appender would feed back into itself + } + this.record("COLLECTOR", "log collector installed, capturing all log4j output"); + } + + private void captureLog(LogEvent event) { + if (CAPTURING.get()) { + return; + } + CAPTURING.set(Boolean.TRUE); + try { + StringBuilder builder = new StringBuilder(128); + builder.append('[').append(event.getLevel()).append("] "); + String loggerName = event.getLoggerName(); + builder.append(loggerName != null ? loggerName : "?").append(": "); + org.apache.logging.log4j.message.Message message = event.getMessage(); + builder.append(message != null ? message.getFormattedMessage() : ""); + Throwable thrown = event.getThrown(); + if (thrown != null) { + java.io.StringWriter writer = new java.io.StringWriter(); + thrown.printStackTrace(new java.io.PrintWriter(writer)); + String stack = writer.toString(); + if (stack.length() > MAX_STACK_CHARS) { + stack = stack.substring(0, MAX_STACK_CHARS) + "\n... (truncated)"; + } + builder.append('\n').append(stack); + } + this.record("GAME", builder.toString()); + } catch (Throwable ignored) { + } finally { + CAPTURING.set(Boolean.FALSE); + } + } + + private void record(String source, String message) { + synchronized (LOCK) { + if (this.lines.size() >= MAX_ENTRIES) { + this.lines.pollFirst(); + } + this.lines.addLast(new Line(System.currentTimeMillis(), source, message)); + } + } + + public int getCount() { + synchronized (LOCK) { + return this.lines.size(); + } + } + + public void clear() { + synchronized (LOCK) { + this.lines.clear(); + } + } + + @EventTarget + public void onPacket(PacketEvent event) { + if (!event.isIncoming()) { + return; + } + try { + Packet packet = event.getPacket(); + if (packet instanceof ClientboundPlayerChatPacket chat) { + Component unsigned = chat.unsignedContent(); + String text = unsigned != null ? unsigned.getString() : chat.body().content(); + this.record("CHAT", this.resolveName(chat.sender()) + ": " + text); + } else if (packet instanceof ClientboundSystemChatPacket system) { + String text = system.content().getString(); + if (!text.isEmpty()) { + this.record("SERVER", text); + } + } else if (packet instanceof ClientboundDisconnectPacket disconnect) { + this.record("NETWORK", "disconnected: " + disconnect.getReason().getString()); + } + } catch (Throwable ignored) { + } + } + + @EventTarget + public void onWorldChange(WorldChangeEvent event) { + this.record("SESSION", "=== world/server changed ==="); + this.environmentLogged = false; + } + + @EventTarget + public void onTick(TickEvent event) { + if (this.environmentLogged || !ZenClient.isReady()) { + return; + } + this.environmentLogged = true; + this.record("SESSION", "=== joined world ==="); + this.recordEnvironment(); + } + + private void recordEnvironment() { + try { + this.record("SESSION", "client: " + ZenClient.CLIENT_NAME + " v" + ZenClient.VERSION + + ", user: " + ZenClient.username); + this.record("SESSION", "minecraft: " + ClientBase.mc.getLaunchedVersion() + + ", os: " + System.getProperty("os.name")); + this.record("SESSION", "game dir: " + ClientBase.mc.gameDirectory.getAbsolutePath()); + ServerData server = ClientBase.mc.getCurrentServer(); + if (server != null) { + this.record("SESSION", "server: " + server.name + " (" + server.ip + ")"); + } else { + this.record("SESSION", "server: (singleplayer)"); + } + } catch (Throwable ignored) { + } + } + + private String resolveName(UUID sender) { + try { + ClientPacketListener connection = ClientBase.mc.getConnection(); + if (connection != null) { + PlayerInfo info = connection.getPlayerInfo(sender); + if (info != null && info.getProfile() != null && info.getProfile().getName() != null) { + return info.getProfile().getName(); + } + } + } catch (Throwable ignored) { + } + return sender.toString(); + } + + /** + * Opens a save dialog and writes the archive; falls back to an automatic + * path under gameDir/logs when the dialog is unavailable or cancelled. + */ + public void exportAndReport() { + String stamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()); + String defaultName = "zen-logs-" + stamp + ".zip"; + File target = FileDialogUtil.saveZip("\u5bfc\u51fa\u65e5\u5fd7", defaultName); + if (target == null) { + target = new File(new File(ClientBase.mc.gameDirectory, "logs"), defaultName); + } + try { + this.export(target); + ChatUtil.print("\u00a7a\u65e5\u5fd7\u5df2\u5bfc\u51fa: " + target.getAbsolutePath()); + } catch (Throwable throwable) { + ChatUtil.print("\u00a7c\u65e5\u5fd7\u5bfc\u51fa\u5931\u8d25: " + throwable); + } + } + + void export(File target) throws IOException { + File parent = target.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("cannot create " + parent); + } + try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(target))) { + this.writeCollected(zip); + this.writeSessionInfo(zip); + this.writeExternalFile(zip, new File(ClientBase.mc.gameDirectory, "logs/latest.log"), "latest.log"); + for (File nativeLog : this.listNativeLogs()) { + this.writeExternalFile(zip, nativeLog, "native/" + nativeLog.getName()); + } + for (File crashReport : this.listCrashReports()) { + this.writeExternalFile(zip, crashReport, "crash-reports/" + crashReport.getName()); + } + } + } + + private void writeCollected(ZipOutputStream zip) throws IOException { + List snapshot; + synchronized (LOCK) { + snapshot = new ArrayList<>(this.lines); + } + SimpleDateFormat full = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + StringBuilder builder = new StringBuilder(64 + snapshot.size() * 48); + builder.append("# Zen log collector - ").append(snapshot.size()).append(" entries\n"); + for (Line line : snapshot) { + builder.append('[').append(full.format(new Date(line.time))).append("] [") + .append(line.source).append("] ").append(line.message).append('\n'); + } + zip.putNextEntry(new ZipEntry("zen-collected.log")); + zip.write(builder.toString().getBytes(StandardCharsets.UTF_8)); + zip.closeEntry(); + } + + private void writeSessionInfo(ZipOutputStream zip) throws IOException { + StringBuilder builder = new StringBuilder(512); + builder.append("client: ").append(ZenClient.CLIENT_NAME).append(" v").append(ZenClient.VERSION).append('\n'); + builder.append("user: ").append(ZenClient.username).append('\n'); + try { + builder.append("minecraft: ").append(ClientBase.mc.getLaunchedVersion()).append('\n'); + builder.append("game dir: ").append(ClientBase.mc.gameDirectory.getAbsolutePath()).append('\n'); + ServerData server = ClientBase.mc.getCurrentServer(); + builder.append("server: ") + .append(server != null ? server.name + " (" + server.ip + ")" : "(singleplayer)").append('\n'); + } catch (Throwable ignored) { + } + builder.append("os: ").append(System.getProperty("os.name")).append('\n'); + builder.append("java: ").append(System.getProperty("java.version")).append('\n'); + builder.append("session start: ").append(new Date(this.sessionStart)).append('\n'); + builder.append("exported at: ").append(new Date()).append('\n'); + builder.append("collected entries: ").append(this.getCount()).append('\n'); + zip.putNextEntry(new ZipEntry("session-info.txt")); + zip.write(builder.toString().getBytes(StandardCharsets.UTF_8)); + zip.closeEntry(); + } + + private List listNativeLogs() { + List result = new ArrayList<>(); + try { + File tmp = new File(System.getProperty("java.io.tmpdir", ".")); + File[] files = tmp.listFiles((dir, name) -> name.startsWith("openzen-") && name.endsWith(".log")); + if (files != null) { + result.addAll(Arrays.asList(files)); + } + } catch (Throwable ignored) { + } + return result; + } + + private List listCrashReports() { + List result = new ArrayList<>(); + try { + File dir = new File(ClientBase.mc.gameDirectory, "crash-reports"); + File[] files = dir.listFiles((dir1, name) -> name.endsWith(".txt")); + if (files != null) { + result.addAll(Arrays.asList(files)); + result.sort(Comparator.comparingLong(File::lastModified).reversed()); + while (result.size() > MAX_CRASH_REPORTS) { + result.remove(result.size() - 1); + } + } + } catch (Throwable ignored) { + } + return result; + } + + private void writeExternalFile(ZipOutputStream zip, File file, String entryName) throws IOException { + if (file == null || !file.isFile()) { + return; + } + byte[] buffer = new byte[8192]; + zip.putNextEntry(new ZipEntry(entryName)); + try (InputStream input = new FileInputStream(file)) { + int read; + while ((read = input.read(buffer)) != -1) { + zip.write(buffer, 0, read); + } + } catch (Throwable ignored) { + } + zip.closeEntry(); + } +} diff --git a/src/main/java/shit/zen/manager/CommandManager.java b/src/main/java/shit/zen/manager/CommandManager.java index 572c81a..b61396f 100644 --- a/src/main/java/shit/zen/manager/CommandManager.java +++ b/src/main/java/shit/zen/manager/CommandManager.java @@ -7,6 +7,7 @@ import shit.zen.command.Command; import shit.zen.command.impl.BindCommand; import shit.zen.command.impl.ConfigCommand; import shit.zen.command.impl.LanguageCommand; +import shit.zen.command.impl.LogCommand; import shit.zen.command.impl.ToggleCommand; import shit.zen.event.impl.ChatEvent; import shit.zen.utils.misc.ChatUtil; @@ -24,6 +25,7 @@ public class CommandManager { this.registerCommand(new BindCommand()); this.registerCommand(new ConfigCommand()); this.registerCommand(new LanguageCommand()); + this.registerCommand(new LogCommand()); this.registerCommand(new ToggleCommand()); } diff --git a/src/main/java/shit/zen/manager/ModuleManager.java b/src/main/java/shit/zen/manager/ModuleManager.java index e1ac486..785560a 100644 --- a/src/main/java/shit/zen/manager/ModuleManager.java +++ b/src/main/java/shit/zen/manager/ModuleManager.java @@ -11,6 +11,7 @@ import shit.zen.event.impl.KeyEvent; import shit.zen.exception.ModuleNotFoundException; import shit.zen.modules.Category; import shit.zen.modules.Module; +import shit.zen.modules.impl.combat.AntiArrow; import shit.zen.modules.impl.combat.AntiBots; import shit.zen.modules.impl.combat.AntiFireball; import shit.zen.modules.impl.combat.AntiKB; @@ -62,6 +63,7 @@ import shit.zen.modules.impl.render.Interface; import shit.zen.modules.impl.render.ItemTags; import shit.zen.modules.impl.render.NameProtect; import shit.zen.modules.impl.render.NameTags; +import shit.zen.modules.impl.render.NoFireOverlay; import shit.zen.modules.impl.render.NoHurtCam; import shit.zen.modules.impl.render.OldHitting; import shit.zen.modules.impl.render.Projectiles; @@ -84,6 +86,7 @@ public class ModuleManager extends ClientBase { public void initModules() { this.register(new AntiBots()); + this.register(new AntiArrow()); this.register(new AntiFireball()); this.register(new AntiKB()); this.register(new AutoOffHand()); @@ -139,6 +142,7 @@ public class ModuleManager extends ClientBase { this.register(new ItemTags()); this.register(new NameProtect()); this.register(new NameTags()); + this.register(new NoFireOverlay()); this.register(new NoHurtCam()); this.register(new OldHitting()); this.register(new Projectiles()); diff --git a/src/main/java/shit/zen/modules/Category.java b/src/main/java/shit/zen/modules/Category.java index ff26d69..d394a78 100644 --- a/src/main/java/shit/zen/modules/Category.java +++ b/src/main/java/shit/zen/modules/Category.java @@ -8,6 +8,7 @@ public enum Category { EXPLOIT("World"), WORLD("Misc"), MISC("Ghost"), + XLZEN("XLZen"), GROUPS("Groups"); public String displayName; diff --git a/src/main/java/shit/zen/modules/impl/combat/AntiArrow.java b/src/main/java/shit/zen/modules/impl/combat/AntiArrow.java new file mode 100644 index 0000000..0b646ce --- /dev/null +++ b/src/main/java/shit/zen/modules/impl/combat/AntiArrow.java @@ -0,0 +1,139 @@ +package shit.zen.modules.impl.combat; + +import java.util.Optional; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.projectile.AbstractArrow; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import shit.zen.event.impl.MotionEvent; +import shit.zen.event.EventTarget; +import shit.zen.modules.Category; +import shit.zen.modules.Module; +import shit.zen.settings.impl.BooleanSetting; +import shit.zen.settings.impl.ModeSetting; +import shit.zen.settings.impl.NumberSetting; + +/** + * Prevents bow arrows from hitting the local player. + * + * The arrow/entity collision itself is simulated server-side (AbstractArrow + * sweeps its per-tick movement segment against the server-side player + * bounding box), so a client cannot cancel a hit directly. What the client + * DOES control is where the server thinks the player is: the player hitbox + * position is taken verbatim from our ServerboundMovePlayerPackets. + * + * Two strategies: + * - Spoof: while a simulated arrow trajectory would intersect the player, + * offset the reported x/z sideways (perpendicular to the arrow's path) + * inside the MotionEvent that LocalPlayerPatch routes into sendPosition. + * The local player never moves; the server-side hitbox does. + * - Dodge: apply a real horizontal velocity impulse perpendicular to the + * arrow's path so the player physically steps out of the way. + * + * Trajectory simulation mirrors vanilla AbstractArrow.tick(): per tick + * pos += vel, then vel *= 0.99 (air drag) and vel.y -= 0.05 (gravity), with + * block collisions truncating the segment, and entity hits tested against + * the player box inflated by 0.3 (ProjectileUtil.getEntityHitResult margin). + */ +public class AntiArrow extends Module { + public static AntiArrow INSTANCE; + + private final ModeSetting modeSetting = new ModeSetting("Mode", "Spoof", "Dodge").withDefault("Spoof"); + private final NumberSetting rangeSetting = new NumberSetting("Range", 32.0, 8.0, 64.0, 1.0); + private final NumberSetting lookaheadSetting = new NumberSetting("Lookahead", 20.0, 5.0, 60.0, 1.0); + private final NumberSetting offsetSetting = new NumberSetting("Offset", 0.9, 0.5, 3.0, 0.1, + () -> this.modeSetting.is("Spoof")); + private final NumberSetting speedSetting = new NumberSetting("Speed", 0.3, 0.1, 1.0, 0.05, + () -> this.modeSetting.is("Dodge")); + private final BooleanSetting ownArrowsSetting = new BooleanSetting("Own Arrows", false); + + public AntiArrow() { + super("AntiArrow", Category.XLZEN); + INSTANCE = this; + } + + private record Threat(AbstractArrow arrow, int impactTicks, Vec3 dodgeDir) { + } + + @EventTarget + public void onMotion(MotionEvent event) { + // The first dispatch (isPost() == true) happens at the head of + // LocalPlayer.sendPosition, before the movement packets are built; + // mutated x/z here flow straight into the outgoing packets. + if (event.isPre()) return; + if (mc.player == null || mc.level == null) return; + + Threat threat = this.findThreat(); + if (threat == null) return; + if (this.modeSetting.is("Dodge")) { + double speed = this.speedSetting.getValue().doubleValue(); + Vec3 motion = mc.player.getDeltaMovement(); + mc.player.setDeltaMovement(threat.dodgeDir().x * speed, motion.y, threat.dodgeDir().z * speed); + } else { + double offset = this.offsetSetting.getValue().doubleValue(); + event.setX(event.getX() + threat.dodgeDir().x * offset); + event.setZ(event.getZ() + threat.dodgeDir().z * offset); + } + } + + /** + * Simulates every nearby arrow's flight and returns the one that will hit + * the player soonest (within the lookahead window), or null if none. + */ + private Threat findThreat() { + double rangeSq = this.rangeSetting.getValue().doubleValue(); + rangeSq *= rangeSq; + int lookahead = this.lookaheadSetting.getValue().intValue(); + Threat best = null; + for (Entity entity : mc.level.entitiesForRendering()) { + if (!(entity instanceof AbstractArrow arrow)) continue; + if (!this.ownArrowsSetting.getValue() && arrow.getOwner() == mc.player) continue; + Vec3 vel = arrow.getDeltaMovement(); + if (vel.lengthSqr() < 0.02) continue; + if (mc.player.distanceToSqr(arrow) > rangeSq) continue; + Vec3 pos = arrow.position(); + AABB playerBox = mc.player.getBoundingBox().inflate(0.3); + Vec3 v = vel; + for (int t = 1; t <= lookahead; t++) { + Vec3 next = pos.add(v); + BlockHitResult blockHit = mc.level.clip(new ClipContext(pos, next, + ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, arrow)); + Vec3 end = blockHit.getType() != HitResult.Type.MISS ? blockHit.getLocation() : next; + if (playerBox.clip(pos, end).isPresent()) { + if (best == null || t < best.impactTicks()) { + best = new Threat(arrow, t, this.perpDodgeDir(pos, v)); + } + break; + } + if (blockHit.getType() != HitResult.Type.MISS) break; + pos = next; + v = v.scale(0.99); + v = new Vec3(v.x, v.y - 0.05, v.z); + } + } + return best; + } + + /** + * Dodge direction: perpendicular to the arrow's horizontal velocity, on + * whichever side of the flight line the player already is (shortest way + * out of the path). + */ + private Vec3 perpDodgeDir(Vec3 arrowPos, Vec3 vel) { + double dx = vel.x; + double dz = vel.z; + double len = Math.sqrt(dx * dx + dz * dz); + if (len < 1.0E-4) return new Vec3(1.0, 0.0, 0.0); + double px = -dz / len; + double pz = dx / len; + double side = (mc.player.getX() - arrowPos.x) * px + (mc.player.getZ() - arrowPos.z) * pz; + if (side < 0.0) { + px = -px; + pz = -pz; + } + return new Vec3(px, 0.0, pz); + } +} diff --git a/src/main/java/shit/zen/modules/impl/render/ESP.java b/src/main/java/shit/zen/modules/impl/render/ESP.java index cc45631..8fcaa49 100644 --- a/src/main/java/shit/zen/modules/impl/render/ESP.java +++ b/src/main/java/shit/zen/modules/impl/render/ESP.java @@ -13,6 +13,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; @@ -29,8 +32,10 @@ import shit.zen.event.impl.Render2DEvent; import shit.zen.event.impl.RenderEvent; import shit.zen.modules.Category; import shit.zen.modules.Module; +import shit.zen.patch.BufferUploaderPatch; import shit.zen.settings.impl.BooleanSetting; import shit.zen.settings.impl.ModeSetting; +import shit.zen.settings.impl.NumberSetting; import shit.zen.utils.game.EntityUtil; import shit.zen.utils.math.Vector2f; import shit.zen.utils.render.ColorUtil; @@ -48,7 +53,11 @@ public class ESP extends Module { } } - private final ModeSetting modeSetting = new ModeSetting("Mode", "Glow", "Outlined 2D").withDefault("Outlined 2D"); + private final ModeSetting modeSetting = new ModeSetting("Mode", "Glow", "Outlined 2D", "Chams").withDefault("Chams"); + private final ModeSetting chamsStyleSetting = new ModeSetting("Chams Style", "Texture", "Color").withDefault("Texture") + .withVisibility(() -> this.modeSetting.is("Chams")); + private final NumberSetting chamsOpacitySetting = new NumberSetting("Chams Opacity", 180.0, 30.0, 255.0, 1.0, + () -> this.modeSetting.is("Chams")); private final BooleanSetting skeletonSetting = new BooleanSetting("Skeleton", false); private final BooleanSetting playersSetting = new BooleanSetting("Players", true); private final BooleanSetting mobsSetting = new BooleanSetting("Mobs", false); @@ -107,7 +116,12 @@ public class ESP extends Module { @EventTarget public void onRender(RenderEvent renderEvent) { if (mc.level == null || mc.player == null) return; - if (!"Outlined 2D".equals(this.modeSetting.getValue())) { + if (this.modeSetting.is("Chams")) { + this.entityBoxPositions.clear(); + this.renderChams(renderEvent); + return; + } + if (!this.modeSetting.is("Outlined 2D")) { this.entityBoxPositions.clear(); return; } @@ -152,6 +166,45 @@ public class ESP extends Module { } } + // Private immediate source for the chams pass only, so endBatch() here + // never flushes vanilla's pending geometry with depth disabled. + private final MultiBufferSource.BufferSource chamsBufferSource = MultiBufferSource.immediate(new BufferBuilder(256)); + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void renderChams(RenderEvent renderEvent) { + float partial = renderEvent.partialTick(); + Vec3 camera = mc.gameRenderer.getMainCamera().getPosition(); + PoseStack poseStack = renderEvent.poseStack(); + float alpha = Mth.clamp(this.chamsOpacitySetting.getValue().floatValue() / 255.0f, 0.05f, 1.0f); + boolean colorStyle = this.chamsStyleSetting.is("Color"); + for (Entity entity : mc.level.entitiesForRendering()) { + if (!(entity instanceof LivingEntity living)) continue; + if (!this.shouldShowEntity(entity) || !this.isInRange(entity)) continue; + EntityRenderer renderer = mc.getEntityRenderDispatcher().getRenderer(living); + Color tint = colorStyle && entity instanceof Player p ? ColorUtil.getPlayerColor(p) : Color.WHITE; + poseStack.pushPose(); + poseStack.translate(living.getX() - camera.x, living.getY() - camera.y, living.getZ() - camera.z); + BufferUploaderPatch.chamsPass.set(Boolean.TRUE); + RenderSystem.setShaderColor(tint.getRed() / 255.0f, tint.getGreen() / 255.0f, + tint.getBlue() / 255.0f, alpha); + try { + // Full renderer path (model + every LayerRenderer: sheep fur, + // armor, held items, ...), so the ghost is identical to the + // real entity. BufferUploaderPatch drops the depth test for + // the flushed batches, making the whole pass visible through + // walls. + renderer.render(living, living.getViewYRot(partial), partial, poseStack, this.chamsBufferSource, + LevelRenderer.getLightColor(mc.level, living.blockPosition())); + this.chamsBufferSource.endBatch(); + } finally { + BufferUploaderPatch.chamsPass.set(Boolean.FALSE); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.enableDepthTest(); + poseStack.popPose(); + } + } + } + @EventTarget public void onRender2D(Render2DEvent event) { if (!"Outlined 2D".equals(this.modeSetting.getValue()) || this.entityBoxPositions.isEmpty()) return; diff --git a/src/main/java/shit/zen/modules/impl/render/NoFireOverlay.java b/src/main/java/shit/zen/modules/impl/render/NoFireOverlay.java new file mode 100644 index 0000000..fdae20c --- /dev/null +++ b/src/main/java/shit/zen/modules/impl/render/NoFireOverlay.java @@ -0,0 +1,13 @@ +package shit.zen.modules.impl.render; + +import shit.zen.modules.Category; +import shit.zen.modules.Module; + +public class NoFireOverlay extends Module { + public static NoFireOverlay INSTANCE; + + public NoFireOverlay() { + super("NoFireOverlay", Category.XLZEN); + INSTANCE = this; + } +} diff --git a/src/main/java/shit/zen/modules/impl/render/Watermark.java b/src/main/java/shit/zen/modules/impl/render/Watermark.java index ddfa75d..a318204 100644 --- a/src/main/java/shit/zen/modules/impl/render/Watermark.java +++ b/src/main/java/shit/zen/modules/impl/render/Watermark.java @@ -1,18 +1,27 @@ package shit.zen.modules.impl.render; +import net.minecraft.network.protocol.game.ClientboundDamageEventPacket; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.player.Player; +import shit.zen.event.impl.EntityHurtEvent; import shit.zen.event.impl.GlRenderEvent; +import shit.zen.event.impl.PacketEvent; import shit.zen.event.impl.Render2DEvent; import shit.zen.hud.DynamicIsland; import shit.zen.hud.NeverloseWatermark; +import shit.zen.hud.TargetHealthHud; import shit.zen.modules.Category; import shit.zen.modules.Module; +import shit.zen.settings.impl.BooleanSetting; import shit.zen.settings.impl.ModeSetting; import shit.zen.event.EventTarget; public class Watermark extends Module { final ModeSetting styleSetting = new ModeSetting("Style", "Neverlose", "DynamicIsland").withDefault("DynamicIsland"); + private final BooleanSetting targetHealthSetting = new BooleanSetting("Target Health", true); private final DynamicIsland dynamicIsland = new DynamicIsland(); private final NeverloseWatermark neverloseWatermark = new NeverloseWatermark(); + private final TargetHealthHud targetHealthHud = new TargetHealthHud(); public Watermark() { super("Watermark", Category.RENDER); @@ -31,6 +40,9 @@ public class Watermark extends Module { this.dynamicIsland.onRender2D(render2DEvent); break; } + if (this.targetHealthSetting.getValue()) { + this.targetHealthHud.render(render2DEvent); + } } @EventTarget @@ -42,4 +54,32 @@ public class Watermark extends Module { this.neverloseWatermark.onGlRender(glRenderEvent); } } + + @EventTarget + public void onPacket(PacketEvent event) { + if (!this.isEnabled() || !event.isIncoming()) { + return; + } + if (event.getPacket() instanceof ClientboundDamageEventPacket packet) { + this.targetHealthHud.onDamagePacket(packet); + } + } + + @EventTarget + public void onEntityHurt(EntityHurtEvent event) { + if (!this.isEnabled()) { + return; + } + DamageSource source = event.damageSource(); + if (source == null) { + return; + } + if (event.entity() == mc.player) { + if (source.getEntity() instanceof Player attacker && attacker != mc.player) { + this.targetHealthHud.note(attacker); + } + } else if (source.getEntity() == mc.player && event.entity() instanceof Player) { + this.targetHealthHud.note(event.entity()); + } + } } diff --git a/src/main/java/shit/zen/patch/BufferUploaderPatch.java b/src/main/java/shit/zen/patch/BufferUploaderPatch.java new file mode 100644 index 0000000..5fa378f --- /dev/null +++ b/src/main/java/shit/zen/patch/BufferUploaderPatch.java @@ -0,0 +1,37 @@ +package shit.zen.patch; + +import asm.patchify.annotation.At; +import asm.patchify.annotation.Inject; +import asm.patchify.annotation.Patch; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; + +@Patch(BufferUploader.class) +public class BufferUploaderPatch { + // Set only around ZenClient's own through-wall (chams) pass, so the depth + // overrides below apply exclusively to the batches flushed by that pass. + public static final ThreadLocal chamsPass = ThreadLocal.withInitial(() -> Boolean.FALSE); + + // drawWithShader is the single funnel every buffered draw goes through and + // runs AFTER RenderType.setupRenderState() / BEFORE the GL draw call, so an + // override here wins over whatever depth state the render type's shards + // just set (a DepthTestStateShard with GL_ALWAYS is a no-op in 1.20.1, and + // any per-call depthFunc tweak before the flush would be clobbered). + @Inject(method = "drawWithShader", desc = "(Lcom/mojang/blaze3d/vertex/BufferBuilder$RenderedBuffer;)V", at = @At(At.Type.HEAD)) + public static void onDrawWithShaderPre(BufferBuilder.RenderedBuffer renderedBuffer, CallbackInfo callbackInfo) { + if (chamsPass.get()) { + RenderSystem.disableDepthTest(); + GlStateManager._depthMask(false); + } + } + + @Inject(method = "drawWithShader", desc = "(Lcom/mojang/blaze3d/vertex/BufferBuilder$RenderedBuffer;)V", at = @At(At.Type.TAIL)) + public static void onDrawWithShaderPost(BufferBuilder.RenderedBuffer renderedBuffer, CallbackInfo callbackInfo) { + if (chamsPass.get()) { + GlStateManager._depthMask(true); + RenderSystem.enableDepthTest(); + } + } +} diff --git a/src/main/java/shit/zen/patch/ScreenEffectRendererPatch.java b/src/main/java/shit/zen/patch/ScreenEffectRendererPatch.java new file mode 100644 index 0000000..53bf573 --- /dev/null +++ b/src/main/java/shit/zen/patch/ScreenEffectRendererPatch.java @@ -0,0 +1,22 @@ +package shit.zen.patch; + +import asm.patchify.annotation.Inject; +import asm.patchify.annotation.Patch; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ScreenEffectRenderer; +import shit.zen.modules.impl.render.NoFireOverlay; + +@Patch(ScreenEffectRenderer.class) +public class ScreenEffectRendererPatch { + // renderFire draws the first-person burning flames over the screen + // (the animated block fire texture). Cancelling it only removes that + // overlay - the underwater / powder-snow passes in the same renderer + // and the actual fire status are untouched. + @Inject(method = "renderFire", desc = "(Lnet/minecraft/client/Minecraft;Lcom/mojang/blaze3d/vertex/PoseStack;)V") + public static void onRenderFire(Minecraft minecraft, PoseStack poseStack, CallbackInfo callbackInfo) { + if (NoFireOverlay.INSTANCE != null && NoFireOverlay.INSTANCE.isEnabled()) { + callbackInfo.cancel(); + } + } +} diff --git a/src/main/java/shit/zen/utils/i18n/Zh.java b/src/main/java/shit/zen/utils/i18n/Zh.java index 1155f62..d1cdcbc 100644 --- a/src/main/java/shit/zen/utils/i18n/Zh.java +++ b/src/main/java/shit/zen/utils/i18n/Zh.java @@ -63,6 +63,7 @@ public final class Zh { put("World", "世界"); put("Misc", "杂项"); put("Ghost", "幽灵"); + put("XLZen", "XLZen"); // --- modules --- put("AimAssist", "瞄准辅助"); @@ -258,6 +259,20 @@ public final class Zh { put("Show Artifacts", "显示神器"); put("Show Health Bar", "显示血条"); put("Health Bar Position", "血条位置"); + put("Glow", "发光"); + put("Outlined 2D", "2D方框"); + put("Chams", "穿墙模型"); + put("Chams Style", "透视样式"); + put("Chams Opacity", "透视不透明度"); + put("Texture", "原材质"); + put("Color", "纯色"); + put("AntiArrow", "防箭"); + put("Spoof", "位置偏移"); + put("Dodge", "移动躲避"); + put("Offset", "偏移距离"); + put("Lookahead", "预判时长"); + put("Own Arrows", "躲避自己的箭"); + put("Target Health", "目标血量"); put("More Particles", "更多粒子"); put("Tracers", "射线"); put("Target ESP", "目标透视");