diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index 54d00b2f..9442e52b 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -24,9 +24,10 @@ dependencies {
compileOnly(group = "io.github.llamalad7", name = "mixinextras-common", version = "0.5.3")
annotationProcessor(group = "io.github.llamalad7", name = "mixinextras-common", version = "0.5.3")
compileOnly(group = "org.ow2.asm", name = "asm", version = "9.8")
+ compileOnly(group = "org.ow2.asm", name = "asm-tree", version = "9.8")
compileOnly(group = "com.google.code.findbugs", name = "jsr305", version = "3.0.2")
- compileOnly("org.bytedeco:javacv-platform:1.5.11")
+// compileOnly("org.bytedeco:javacv-platform:1.5.11")
}
configurations {
@@ -64,3 +65,12 @@ sourceSets.configureEach {
}
}
}
+
+/*tasks.register("extractRuntimeClasspath") {
+ from(configurations.runtimeClasspath)
+ into("$projectDir/build/runtimeClasspath")
+
+ doFirst {
+ file("$projectDir/build/runtimeClasspath").mkdirs()
+ }
+}*/
diff --git a/common/src/main/java/com/github/epsilon/graphics/video/VideoManager.java b/common/src/main/java/com/github/epsilon/graphics/video/VideoManager.java
index 0bb9d991..2d938405 100644
--- a/common/src/main/java/com/github/epsilon/graphics/video/VideoManager.java
+++ b/common/src/main/java/com/github/epsilon/graphics/video/VideoManager.java
@@ -1,41 +1,41 @@
-package com.github.epsilon.graphics.video;
-
-import com.github.epsilon.Epsilon;
-import com.mojang.logging.LogUtils;
-import net.minecraft.client.Minecraft;
-import org.apache.commons.io.IOUtils;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.Objects;
-
-public class VideoManager {
-
- private static final File directory = new File(Minecraft.getInstance().gameDirectory, "Sakura/Background");
- private static final File backgroundFile = new File(directory, "background.mp4");
-
- private static void unpackFile(File file, String name) throws IOException {
- FileOutputStream fos = new FileOutputStream(file);
- IOUtils.copy(Objects.requireNonNull(Epsilon.class.getClassLoader().getResourceAsStream(name)), fos);
- fos.close();
- }
-
- public static void ensureBackgroundFile() throws IOException {
- if (!directory.exists()) {
- directory.mkdirs();
- }
- if (!backgroundFile.exists()) {
- unpackFile(backgroundFile, "assets/epsilon/background/wallpaper.mp4");
- }
- }
-
- public static void loadBackground(int fps) throws IOException {
- if (!backgroundFile.exists()) {
- LogUtils.getLogger().error("Background file not found, this should not happen! Reload files.");
- ensureBackgroundFile();
- }
- VideoUtil.init(backgroundFile, fps);
- }
-
-}
+//package com.github.epsilon.graphics.video;
+//
+//import com.github.epsilon.Epsilon;
+//import com.mojang.logging.LogUtils;
+//import net.minecraft.client.Minecraft;
+//import org.apache.commons.io.IOUtils;
+//
+//import java.io.File;
+//import java.io.FileOutputStream;
+//import java.io.IOException;
+//import java.util.Objects;
+//
+//public class VideoManager {
+//
+// private static final File directory = new File(Minecraft.getInstance().gameDirectory, "Sakura/Background");
+// private static final File backgroundFile = new File(directory, "background.mp4");
+//
+// private static void unpackFile(File file, String name) throws IOException {
+// FileOutputStream fos = new FileOutputStream(file);
+// IOUtils.copy(Objects.requireNonNull(Epsilon.class.getClassLoader().getResourceAsStream(name)), fos);
+// fos.close();
+// }
+//
+// public static void ensureBackgroundFile() throws IOException {
+// if (!directory.exists()) {
+// directory.mkdirs();
+// }
+// if (!backgroundFile.exists()) {
+// unpackFile(backgroundFile, "assets/epsilon/background/wallpaper.mp4");
+// }
+// }
+//
+// public static void loadBackground(int fps) throws IOException {
+// if (!backgroundFile.exists()) {
+// LogUtils.getLogger().error("Background file not found, this should not happen! Reload files.");
+// ensureBackgroundFile();
+// }
+// VideoUtil.init(backgroundFile, fps);
+// }
+//
+//}
diff --git a/common/src/main/java/com/github/epsilon/graphics/video/VideoUtil.java b/common/src/main/java/com/github/epsilon/graphics/video/VideoUtil.java
index 11524837..eec3e571 100644
--- a/common/src/main/java/com/github/epsilon/graphics/video/VideoUtil.java
+++ b/common/src/main/java/com/github/epsilon/graphics/video/VideoUtil.java
@@ -1,383 +1,383 @@
-package com.github.epsilon.graphics.video;
-
-import com.github.epsilon.Epsilon;
-import com.github.epsilon.assets.resources.ResourceLocationUtils;
-import com.mojang.blaze3d.platform.NativeImage;
-import com.mojang.blaze3d.systems.CommandEncoder;
-import com.mojang.blaze3d.systems.RenderSystem;
-import com.mojang.blaze3d.textures.GpuTextureView;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.texture.DynamicTexture;
-import net.minecraft.resources.Identifier;
-import org.bytedeco.javacv.FFmpegFrameGrabber;
-import org.bytedeco.javacv.Frame;
-
-import java.io.File;
-import java.nio.ByteBuffer;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.LockSupport;
-
-import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_RGBA;
-
-public class VideoUtil {
-
- private static final Minecraft mc = Minecraft.getInstance();
-
- private static final VideoGpuTexture VIDEO_TEX = new VideoGpuTexture();
-
- private static final Identifier GUI_TEXTURE_ID = ResourceLocationUtils.getIdentifier("dynamic/title_video");
-
- private static DynamicTexture registeredTexture;
-
- private static FFmpegFrameGrabber grabber;
-
- private static double frameRate;
-
- private static long mediaStartUs;
- private static long wallStartNs;
-
-
- private static volatile long lastPtsUs = 0;
-
- private static ExecutorService executor;
- private static Future> decodeFuture;
-
- private static final Object pauseLock = new Object();
-
- private static volatile boolean paused = false;
- private static volatile boolean stopped = true;
-
- private static final Object GRABBER_LOCK = new Object();
-
- private static final AtomicReference latestFrame = new AtomicReference<>();
-
- private static final int POOL = 4;
- private static final ByteBuffer[] pool = new ByteBuffer[POOL];
- private static int poolCursor = 0;
-
- private static void releaseGuiTexture() {
- if (registeredTexture == null) {
- return;
- }
- mc.getTextureManager().release(GUI_TEXTURE_ID);
- registeredTexture = null;
- }
-
- private static ByteBuffer acquire(int size) {
- FrameBuffer cur = latestFrame.get();
- ByteBuffer curBuf = cur != null ? cur.buffer : null;
-
- for (int n = 0; n < POOL; n++) {
- int idx = (poolCursor + n) % POOL;
- ByteBuffer b = pool[idx];
-
- if (b == curBuf) continue;
- if (b == null || b.capacity() < size) {
- b = ByteBuffer.allocateDirect(size);
- pool[idx] = b;
- }
-
- poolCursor = (idx + 1) % POOL;
- return b;
- }
-
- return ByteBuffer.allocateDirect(size);
- }
-
- private static final class VideoGpuTexture {
- private DynamicTexture tex;
- private int w = -1, h = -1;
-
- void close() {
- if (tex != null) {
- if (registeredTexture == tex) {
- releaseGuiTexture();
- } else {
- try {
- tex.close();
- } catch (Throwable ignored) {
- }
- }
- tex = null;
- }
- w = h = -1;
- }
-
- GpuTextureView view() {
- return tex != null ? tex.getTextureView() : null;
- }
-
- DynamicTexture texture() {
- return tex;
- }
-
- int width() {
- return w;
- }
-
- int height() {
- return h;
- }
-
- void ensureSize(int newW, int newH) {
- if (tex != null && w == newW && h == newH) return;
-
- close();
- tex = new DynamicTexture("video", newW, newH, true);
- w = newW;
- h = newH;
- }
-
- void uploadRgba(ByteBuffer rgba, int frameW, int frameH) {
- ensureSize(frameW, frameH);
-
- ByteBuffer src = rgba.duplicate();
- src.rewind();
-
- CommandEncoder enc = RenderSystem.getDevice().createCommandEncoder();
- enc.writeToTexture(
- tex.getTexture(),
- src,
- NativeImage.Format.RGBA,
- 0,
- 0,
- 0,
- 0,
- frameW,
- frameH
- );
- }
- }
-
- public static void init(File file, int fps) {
- stop();
-
- try {
- Epsilon.LOGGER.info("[VideoPlayer] Initializing... {}", file.getAbsolutePath());
-
- executor = Executors.newSingleThreadExecutor(r -> {
- Thread t = new Thread(r, "VideoUtil-Decode");
- t.setDaemon(true);
- return t;
- });
-
- synchronized (GRABBER_LOCK) {
- grabber = new FFmpegFrameGrabber(file);
- grabber.setPixelFormat(AV_PIX_FMT_RGBA);
- grabber.setOption("threads", "4");
- grabber.start();
-
- frameRate = (fps == -1 ? grabber.getFrameRate() : fps);
-
- Frame first = grabber.grabImage();
- if (first != null && first.image != null) {
- FrameBuffer fb = copyFrameLocked(first);
- latestFrame.set(fb);
- lastPtsUs = (first.timestamp > 0 ? first.timestamp : grabber.getTimestamp());
- }
-
- mediaStartUs = grabber.getTimestamp();
- wallStartNs = System.nanoTime();
- lastPtsUs = mediaStartUs;
- }
-
- stopped = false;
- paused = false;
-
- startDecodeThread();
- } catch (Throwable e) {
- Epsilon.LOGGER.error("[VideoPlayer] Init error:", e);
- stop();
- }
- }
-
- public static void pause() {
- paused = true;
- }
-
- public static void resume() {
- if (!paused) return;
-
- synchronized (pauseLock) {
- paused = false;
- wallStartNs = System.nanoTime() - (lastPtsUs - mediaStartUs) * 1000L;
- pauseLock.notifyAll();
- }
- }
-
- public static void stop() {
- try {
- if (stopped && grabber == null) return;
-
- stopped = true;
-
- synchronized (pauseLock) {
- paused = false;
- pauseLock.notifyAll();
- }
-
- if (decodeFuture != null) {
- decodeFuture.cancel(true);
- decodeFuture = null;
- }
-
- if (executor != null) {
- executor.shutdown();
- executor.awaitTermination(2, TimeUnit.SECONDS);
- executor = null;
- }
-
- synchronized (GRABBER_LOCK) {
- if (grabber != null) {
- try {
- grabber.stop();
- } catch (Throwable ignored) {
- }
- try {
- grabber.close();
- } catch (Throwable ignored) {
- }
- grabber = null;
- }
- }
-
- latestFrame.set(null);
-
-
- Runnable closeGpu = VIDEO_TEX::close;
- if (RenderSystem.isOnRenderThread()) {
- closeGpu.run();
- } else {
- RenderSystem.queueFencedTask(closeGpu);
- }
-
- Epsilon.LOGGER.info("[VideoPlayer] Stopped");
- } catch (Throwable e) {
- Epsilon.LOGGER.error("[VideoPlayer] Stop error:", e);
- }
- }
-
- public static Identifier getGuiTexture() {
- if (stopped || paused) return null;
- if (!RenderSystem.isOnRenderThread()) return null;
-
- FrameBuffer frame = latestFrame.get();
- if (frame == null) return null;
-
- VIDEO_TEX.uploadRgba(frame.buffer, frame.width, frame.height);
-
- DynamicTexture texture = VIDEO_TEX.texture();
- if (texture == null) return null;
-
- if (registeredTexture != texture) {
- mc.getTextureManager().register(GUI_TEXTURE_ID, texture);
- registeredTexture = texture;
- }
-
- return GUI_TEXTURE_ID;
- }
-
- public static int getGuiTextureWidth() {
- int width = VIDEO_TEX.width();
- if (width > 0) {
- return width;
- }
- FrameBuffer frame = latestFrame.get();
- return frame != null ? frame.width : -1;
- }
-
- public static int getGuiTextureHeight() {
- int height = VIDEO_TEX.height();
- if (height > 0) {
- return height;
- }
- FrameBuffer frame = latestFrame.get();
- return frame != null ? frame.height : -1;
- }
-
- private static void startDecodeThread() {
- decodeFuture = executor.submit(() -> {
- try {
- while (!stopped) {
- if (paused) {
- synchronized (pauseLock) {
- while (paused && !stopped) pauseLock.wait();
- }
- if (stopped) break;
- }
-
- Frame frame;
- long ptsUs;
-
- synchronized (GRABBER_LOCK) {
- if (stopped || grabber == null) break;
-
- frame = grabber.grabImage();
- if (frame == null || frame.image == null) {
- grabber.setTimestamp(0);
- Frame first = grabber.grabImage();
- if (first != null && first.image != null) {
- ptsUs = (first.timestamp > 0 ? first.timestamp : grabber.getTimestamp());
- lastPtsUs = ptsUs;
-
- latestFrame.set(copyFrameLocked(first));
- mediaStartUs = grabber.getTimestamp();
- wallStartNs = System.nanoTime();
- }
- continue;
- }
-
- ptsUs = (frame.timestamp > 0 ? frame.timestamp : grabber.getTimestamp());
- lastPtsUs = ptsUs;
-
- latestFrame.set(copyFrameLocked(frame));
- }
-
- long targetNs = wallStartNs + (ptsUs - mediaStartUs) * 1000L;
- long sleepNs = targetNs - System.nanoTime();
- if (sleepNs > 0) {
- LockSupport.parkNanos(Math.min(sleepNs, 5_000_000L));
- }
- }
- } catch (InterruptedException ignored) {
- } catch (Throwable e) {
- Epsilon.LOGGER.error("[VideoPlayer] Decode thread error:", e);
- }
- });
- }
-
- private static FrameBuffer copyFrameLocked(Frame frame) {
- int w = frame.imageWidth;
- int h = frame.imageHeight;
-
- int channels = 4;
- int size = w * h * channels;
-
- ByteBuffer src = (ByteBuffer) frame.image[0];
-
- ByteBuffer s = src.duplicate();
- s.rewind();
-
-
- if (s.remaining() < size) {
- size = s.remaining();
- } else if (s.remaining() > size) {
- s.limit(size);
- }
-
- ByteBuffer dst = acquire(size);
- dst.clear();
- dst.put(s);
- dst.flip();
-
- return new FrameBuffer(dst, w, h);
- }
-
- private record FrameBuffer(ByteBuffer buffer, int width, int height) {
- }
-
-}
+//package com.github.epsilon.graphics.video;
+//
+//import com.github.epsilon.Epsilon;
+//import com.github.epsilon.assets.resources.ResourceLocationUtils;
+//import com.mojang.blaze3d.platform.NativeImage;
+//import com.mojang.blaze3d.systems.CommandEncoder;
+//import com.mojang.blaze3d.systems.RenderSystem;
+//import com.mojang.blaze3d.textures.GpuTextureView;
+//import net.minecraft.client.Minecraft;
+//import net.minecraft.client.renderer.texture.DynamicTexture;
+//import net.minecraft.resources.Identifier;
+//import org.bytedeco.javacv.FFmpegFrameGrabber;
+//import org.bytedeco.javacv.Frame;
+//
+//import java.io.File;
+//import java.nio.ByteBuffer;
+//import java.util.concurrent.ExecutorService;
+//import java.util.concurrent.Executors;
+//import java.util.concurrent.Future;
+//import java.util.concurrent.TimeUnit;
+//import java.util.concurrent.atomic.AtomicReference;
+//import java.util.concurrent.locks.LockSupport;
+//
+//import static org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_RGBA;
+//
+//public class VideoUtil {
+//
+// private static final Minecraft mc = Minecraft.getInstance();
+//
+// private static final VideoGpuTexture VIDEO_TEX = new VideoGpuTexture();
+//
+// private static final Identifier GUI_TEXTURE_ID = ResourceLocationUtils.getIdentifier("dynamic/title_video");
+//
+// private static DynamicTexture registeredTexture;
+//
+// private static FFmpegFrameGrabber grabber;
+//
+// private static double frameRate;
+//
+// private static long mediaStartUs;
+// private static long wallStartNs;
+//
+//
+// private static volatile long lastPtsUs = 0;
+//
+// private static ExecutorService executor;
+// private static Future> decodeFuture;
+//
+// private static final Object pauseLock = new Object();
+//
+// private static volatile boolean paused = false;
+// private static volatile boolean stopped = true;
+//
+// private static final Object GRABBER_LOCK = new Object();
+//
+// private static final AtomicReference latestFrame = new AtomicReference<>();
+//
+// private static final int POOL = 4;
+// private static final ByteBuffer[] pool = new ByteBuffer[POOL];
+// private static int poolCursor = 0;
+//
+// private static void releaseGuiTexture() {
+// if (registeredTexture == null) {
+// return;
+// }
+// mc.getTextureManager().release(GUI_TEXTURE_ID);
+// registeredTexture = null;
+// }
+//
+// private static ByteBuffer acquire(int size) {
+// FrameBuffer cur = latestFrame.get();
+// ByteBuffer curBuf = cur != null ? cur.buffer : null;
+//
+// for (int n = 0; n < POOL; n++) {
+// int idx = (poolCursor + n) % POOL;
+// ByteBuffer b = pool[idx];
+//
+// if (b == curBuf) continue;
+// if (b == null || b.capacity() < size) {
+// b = ByteBuffer.allocateDirect(size);
+// pool[idx] = b;
+// }
+//
+// poolCursor = (idx + 1) % POOL;
+// return b;
+// }
+//
+// return ByteBuffer.allocateDirect(size);
+// }
+//
+// private static final class VideoGpuTexture {
+// private DynamicTexture tex;
+// private int w = -1, h = -1;
+//
+// void close() {
+// if (tex != null) {
+// if (registeredTexture == tex) {
+// releaseGuiTexture();
+// } else {
+// try {
+// tex.close();
+// } catch (Throwable ignored) {
+// }
+// }
+// tex = null;
+// }
+// w = h = -1;
+// }
+//
+// GpuTextureView view() {
+// return tex != null ? tex.getTextureView() : null;
+// }
+//
+// DynamicTexture texture() {
+// return tex;
+// }
+//
+// int width() {
+// return w;
+// }
+//
+// int height() {
+// return h;
+// }
+//
+// void ensureSize(int newW, int newH) {
+// if (tex != null && w == newW && h == newH) return;
+//
+// close();
+// tex = new DynamicTexture("video", newW, newH, true);
+// w = newW;
+// h = newH;
+// }
+//
+// void uploadRgba(ByteBuffer rgba, int frameW, int frameH) {
+// ensureSize(frameW, frameH);
+//
+// ByteBuffer src = rgba.duplicate();
+// src.rewind();
+//
+// CommandEncoder enc = RenderSystem.getDevice().createCommandEncoder();
+// enc.writeToTexture(
+// tex.getTexture(),
+// src,
+// NativeImage.Format.RGBA,
+// 0,
+// 0,
+// 0,
+// 0,
+// frameW,
+// frameH
+// );
+// }
+// }
+//
+// public static void init(File file, int fps) {
+// stop();
+//
+// try {
+// Epsilon.LOGGER.info("[VideoPlayer] Initializing... {}", file.getAbsolutePath());
+//
+// executor = Executors.newSingleThreadExecutor(r -> {
+// Thread t = new Thread(r, "VideoUtil-Decode");
+// t.setDaemon(true);
+// return t;
+// });
+//
+// synchronized (GRABBER_LOCK) {
+// grabber = new FFmpegFrameGrabber(file);
+// grabber.setPixelFormat(AV_PIX_FMT_RGBA);
+// grabber.setOption("threads", "4");
+// grabber.start();
+//
+// frameRate = (fps == -1 ? grabber.getFrameRate() : fps);
+//
+// Frame first = grabber.grabImage();
+// if (first != null && first.image != null) {
+// FrameBuffer fb = copyFrameLocked(first);
+// latestFrame.set(fb);
+// lastPtsUs = (first.timestamp > 0 ? first.timestamp : grabber.getTimestamp());
+// }
+//
+// mediaStartUs = grabber.getTimestamp();
+// wallStartNs = System.nanoTime();
+// lastPtsUs = mediaStartUs;
+// }
+//
+// stopped = false;
+// paused = false;
+//
+// startDecodeThread();
+// } catch (Throwable e) {
+// Epsilon.LOGGER.error("[VideoPlayer] Init error:", e);
+// stop();
+// }
+// }
+//
+// public static void pause() {
+// paused = true;
+// }
+//
+// public static void resume() {
+// if (!paused) return;
+//
+// synchronized (pauseLock) {
+// paused = false;
+// wallStartNs = System.nanoTime() - (lastPtsUs - mediaStartUs) * 1000L;
+// pauseLock.notifyAll();
+// }
+// }
+//
+// public static void stop() {
+// try {
+// if (stopped && grabber == null) return;
+//
+// stopped = true;
+//
+// synchronized (pauseLock) {
+// paused = false;
+// pauseLock.notifyAll();
+// }
+//
+// if (decodeFuture != null) {
+// decodeFuture.cancel(true);
+// decodeFuture = null;
+// }
+//
+// if (executor != null) {
+// executor.shutdown();
+// executor.awaitTermination(2, TimeUnit.SECONDS);
+// executor = null;
+// }
+//
+// synchronized (GRABBER_LOCK) {
+// if (grabber != null) {
+// try {
+// grabber.stop();
+// } catch (Throwable ignored) {
+// }
+// try {
+// grabber.close();
+// } catch (Throwable ignored) {
+// }
+// grabber = null;
+// }
+// }
+//
+// latestFrame.set(null);
+//
+//
+// Runnable closeGpu = VIDEO_TEX::close;
+// if (RenderSystem.isOnRenderThread()) {
+// closeGpu.run();
+// } else {
+// RenderSystem.queueFencedTask(closeGpu);
+// }
+//
+// Epsilon.LOGGER.info("[VideoPlayer] Stopped");
+// } catch (Throwable e) {
+// Epsilon.LOGGER.error("[VideoPlayer] Stop error:", e);
+// }
+// }
+//
+// public static Identifier getGuiTexture() {
+// if (stopped || paused) return null;
+// if (!RenderSystem.isOnRenderThread()) return null;
+//
+// FrameBuffer frame = latestFrame.get();
+// if (frame == null) return null;
+//
+// VIDEO_TEX.uploadRgba(frame.buffer, frame.width, frame.height);
+//
+// DynamicTexture texture = VIDEO_TEX.texture();
+// if (texture == null) return null;
+//
+// if (registeredTexture != texture) {
+// mc.getTextureManager().register(GUI_TEXTURE_ID, texture);
+// registeredTexture = texture;
+// }
+//
+// return GUI_TEXTURE_ID;
+// }
+//
+// public static int getGuiTextureWidth() {
+// int width = VIDEO_TEX.width();
+// if (width > 0) {
+// return width;
+// }
+// FrameBuffer frame = latestFrame.get();
+// return frame != null ? frame.width : -1;
+// }
+//
+// public static int getGuiTextureHeight() {
+// int height = VIDEO_TEX.height();
+// if (height > 0) {
+// return height;
+// }
+// FrameBuffer frame = latestFrame.get();
+// return frame != null ? frame.height : -1;
+// }
+//
+// private static void startDecodeThread() {
+// decodeFuture = executor.submit(() -> {
+// try {
+// while (!stopped) {
+// if (paused) {
+// synchronized (pauseLock) {
+// while (paused && !stopped) pauseLock.wait();
+// }
+// if (stopped) break;
+// }
+//
+// Frame frame;
+// long ptsUs;
+//
+// synchronized (GRABBER_LOCK) {
+// if (stopped || grabber == null) break;
+//
+// frame = grabber.grabImage();
+// if (frame == null || frame.image == null) {
+// grabber.setTimestamp(0);
+// Frame first = grabber.grabImage();
+// if (first != null && first.image != null) {
+// ptsUs = (first.timestamp > 0 ? first.timestamp : grabber.getTimestamp());
+// lastPtsUs = ptsUs;
+//
+// latestFrame.set(copyFrameLocked(first));
+// mediaStartUs = grabber.getTimestamp();
+// wallStartNs = System.nanoTime();
+// }
+// continue;
+// }
+//
+// ptsUs = (frame.timestamp > 0 ? frame.timestamp : grabber.getTimestamp());
+// lastPtsUs = ptsUs;
+//
+// latestFrame.set(copyFrameLocked(frame));
+// }
+//
+// long targetNs = wallStartNs + (ptsUs - mediaStartUs) * 1000L;
+// long sleepNs = targetNs - System.nanoTime();
+// if (sleepNs > 0) {
+// LockSupport.parkNanos(Math.min(sleepNs, 5_000_000L));
+// }
+// }
+// } catch (InterruptedException ignored) {
+// } catch (Throwable e) {
+// Epsilon.LOGGER.error("[VideoPlayer] Decode thread error:", e);
+// }
+// });
+// }
+//
+// private static FrameBuffer copyFrameLocked(Frame frame) {
+// int w = frame.imageWidth;
+// int h = frame.imageHeight;
+//
+// int channels = 4;
+// int size = w * h * channels;
+//
+// ByteBuffer src = (ByteBuffer) frame.image[0];
+//
+// ByteBuffer s = src.duplicate();
+// s.rewind();
+//
+//
+// if (s.remaining() < size) {
+// size = s.remaining();
+// } else if (s.remaining() > size) {
+// s.limit(size);
+// }
+//
+// ByteBuffer dst = acquire(size);
+// dst.clear();
+// dst.put(s);
+// dst.flip();
+//
+// return new FrameBuffer(dst, w, h);
+// }
+//
+// private record FrameBuffer(ByteBuffer buffer, int width, int height) {
+// }
+//
+//}
diff --git a/common/src/main/java/com/github/epsilon/gui/screen/MainMenuScreen.java b/common/src/main/java/com/github/epsilon/gui/screen/MainMenuScreen.java
index 0d807bcf..858f6182 100644
--- a/common/src/main/java/com/github/epsilon/gui/screen/MainMenuScreen.java
+++ b/common/src/main/java/com/github/epsilon/gui/screen/MainMenuScreen.java
@@ -1,382 +1,382 @@
-package com.github.epsilon.gui.screen;
-
-import com.github.epsilon.Epsilon;
-import com.github.epsilon.graphics.LuminRenderSystem;
-import com.github.epsilon.graphics.renderers.RoundRectRenderer;
-import com.github.epsilon.graphics.renderers.ShadowRenderer;
-import com.github.epsilon.graphics.renderers.TextRenderer;
-import com.github.epsilon.graphics.text.StaticFontLoader;
-import com.github.epsilon.graphics.video.VideoManager;
-import com.github.epsilon.graphics.video.VideoUtil;
-import com.github.epsilon.gui.panel.MD3Theme;
-import net.minecraft.client.gui.GuiGraphicsExtractor;
-import net.minecraft.client.gui.screens.Screen;
-import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
-import net.minecraft.client.gui.screens.multiplayer.SafetyScreen;
-import net.minecraft.client.gui.screens.options.OptionsScreen;
-import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
-import net.minecraft.client.input.MouseButtonEvent;
-import net.minecraft.network.chat.Component;
-import net.minecraft.resources.Identifier;
-import net.minecraft.util.Mth;
-import net.minecraft.util.Util;
-
-import java.awt.*;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class MainMenuScreen extends Screen {
-
- private static final float VIDEO_ZOOM = 1.08f;
- private static final float VIDEO_PARALLAX_STRENGTH = 0.35f;
-
- public static final MainMenuScreen INSTANCE = new MainMenuScreen();
-
- private final ShadowRenderer shadowRenderer = new ShadowRenderer();
- private final RoundRectRenderer roundRectRenderer = new RoundRectRenderer();
- private final TextRenderer textRenderer = new TextRenderer();
-
- private final List entries = new ArrayList<>();
-
- private LuminRenderSystem.LuminRenderTarget renderTarget;
-
- private boolean videoStarted;
- private long introStartMs;
-
- private MainMenuScreen() {
- super(Component.literal("MainMenuScreen"));
- entries.add(new MenuEntry("S", "Singleplayer", "Create and manage your worlds", () -> minecraft.setScreen(new SelectWorldScreen(this))));
- entries.add(new MenuEntry("M", "Multiplayer", "Servers, friends and online play", () -> {
- Screen screen = this.minecraft.options.skipMultiplayerWarning ? new JoinMultiplayerScreen(this) : new SafetyScreen(this);
- this.minecraft.setScreen(screen);
- }));
- entries.add(new MenuEntry("O", "Options", "Video, controls and client settings", () -> minecraft.setScreen(new OptionsScreen(this, minecraft.options, false))));
- entries.add(new MenuEntry("Q", "Quit", "Leave Epsilon and close the game", minecraft::stop));
- }
-
- @Override
- protected void init() {
- super.init();
- introStartMs = Util.getMillis();
- for (MenuEntry entry : entries) {
- entry.hoverProgress = 0.0f;
- entry.setBounds(0.0f, 0.0f, 0.0f, 0.0f);
- }
- }
-
- @Override
- public void added() {
- super.added();
-
- if (videoStarted) {
- return;
- }
-
- try {
- VideoManager.loadBackground(60);
- videoStarted = true;
- } catch (IOException e) {
- Epsilon.LOGGER.error("MainMenu视频加载失败!", e);
- }
- }
-
- @Override
- public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
- Identifier videoTexture = videoStarted ? VideoUtil.getGuiTexture() : null;
- if (videoTexture != null) {
- int videoWidth = VideoUtil.getGuiTextureWidth();
- int videoHeight = VideoUtil.getGuiTextureHeight();
- if (videoWidth > 0 && videoHeight > 0) {
- float screenAspect = (float) this.width / (float) this.height;
- float videoAspect = (float) videoWidth / (float) videoHeight;
-
- float visibleWidth = 1.0f;
- float visibleHeight = 1.0f;
- if (videoAspect > screenAspect) {
- visibleWidth = screenAspect / videoAspect;
- } else {
- visibleHeight = videoAspect / screenAspect;
- }
-
- visibleWidth /= VIDEO_ZOOM;
- visibleHeight /= VIDEO_ZOOM;
-
- float mouseNormX = this.width <= 0 ? 0.0f : Mth.clamp((mouseX / (float) this.width) * 2.0f - 1.0f, -1.0f, 1.0f);
- float mouseNormY = this.height <= 0 ? 0.0f : Mth.clamp((mouseY / (float) this.height) * 2.0f - 1.0f, -1.0f, 1.0f);
-
- float uRange = 1.0f - visibleWidth;
- float vRange = 1.0f - visibleHeight;
- float u0 = Mth.clamp((uRange * 0.5f) - mouseNormX * uRange * VIDEO_PARALLAX_STRENGTH, 0.0f, uRange);
- float v0 = Mth.clamp((vRange * 0.5f) - mouseNormY * vRange * VIDEO_PARALLAX_STRENGTH, 0.0f, vRange);
- float u1 = u0 + visibleWidth;
- float v1 = v0 + visibleHeight;
-
- graphics.blit(videoTexture, 0, 0, this.width, this.height, u0, u1, v0, v1);
- return;
- }
- return;
- }
-
- // 视频已经启动但当前帧尚未准备好时,回退到全景背景,避免黑屏。
- this.minecraft.gameRenderer.getPanorama().extractRenderState(graphics, this.width, this.height, this.panoramaShouldSpin());
- }
-
- @Override
- public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
- final var window = minecraft.getWindow();
- if (renderTarget == null) {
- renderTarget = LuminRenderSystem.LuminRenderTarget.create("main-menu", window.getWidth(), window.getHeight());
- }
-
- renderTarget.clear();
- renderTarget.resize(window.getWidth(), window.getHeight());
- LuminRenderSystem.setActiveTarget(renderTarget);
-
- MD3Theme.syncFromSettings();
- drawMenu(mouseX, mouseY);
-
- LuminRenderSystem.setActiveTarget(null);
- graphics.blit(renderTarget.getIdentifier(), 0, 0, window.getGuiScaledWidth(), window.getGuiScaledHeight(), 0, 1, 1, 0);
- }
-
- private void drawMenu(int mouseX, int mouseY) {
- long now = Util.getMillis();
- float introProgress = easeOutCubic(Mth.clamp((now - introStartMs) / 650.0f, 0.0f, 1.0f));
- Layout layout = Layout.resolve(width, height, entries.size());
-
- Color panelShadow = applyAlpha(MD3Theme.SHADOW, 0.90f);
- Color panelColor = applyAlpha(MD3Theme.SURFACE, 0.74f);
- Color titleColor = applyAlpha(MD3Theme.TEXT_PRIMARY, 0.96f);
- Color subtitleColor = applyAlpha(MD3Theme.TEXT_SECONDARY, 0.90f);
-
- shadowRenderer.addShadow(layout.panelX, layout.panelY, layout.panelWidth, layout.panelHeight, layout.panelRadius, 22.0f * layout.scale, panelShadow);
- roundRectRenderer.addRoundRect(layout.panelX, layout.panelY, layout.panelWidth, layout.panelHeight, layout.panelRadius, panelColor);
-
- float titleScale = 1.04f * layout.scale;
- float versionScale = 0.56f * layout.scale;
- float titleY = layout.panelY + layout.padding + 2.0f * layout.scale;
- float subtitleY = titleY + textRenderer.getLineHeight(titleScale, StaticFontLoader.DUCKSANS) + 2.0f * layout.scale;
-
- textRenderer.addText("Open Epsilon", layout.panelX + layout.padding, titleY, titleScale, titleColor, StaticFontLoader.DUCKSANS);
- textRenderer.addText("Minecraft 26.1.2 | " + Epsilon.VERSION, layout.panelX + layout.padding, subtitleY, versionScale, subtitleColor);
-
- for (int i = 0; i < entries.size(); i++) {
- renderEntry(entries.get(i), i, mouseX, mouseY, introProgress, layout);
- }
-
- shadowRenderer.drawAndClear();
- roundRectRenderer.drawAndClear();
- textRenderer.drawAndClear();
- }
-
- private void renderEntry(MenuEntry entry, int index, int mouseX, int mouseY, float introProgress, Layout layout) {
- float staged = Mth.clamp((introProgress - index * 0.09f) / 0.60f, 0.0f, 1.0f);
- float appear = easeOutCubic(staged);
- if (appear <= 0.001f) {
- entry.setBounds(0.0f, 0.0f, 0.0f, 0.0f);
- return;
- }
-
- float y = layout.buttonsY + index * (layout.buttonHeight + layout.buttonGap);
- float slideX = (1.0f - appear) * (-18.0f * layout.scale);
- float drawX = layout.buttonsX + slideX;
-
- entry.setBounds(drawX, y, layout.buttonWidth, layout.buttonHeight);
-
- boolean hovered = entry.isHovered(mouseX, mouseY);
- entry.hoverProgress = Mth.lerp(hovered ? 0.24f : 0.16f, entry.hoverProgress, hovered ? 1.0f : 0.0f);
-
- float hover = entry.hoverProgress;
- float cardLift = hover * 2.0f * layout.scale;
- float cardY = y - cardLift;
-
- Color baseColor = applyAlpha(MD3Theme.SURFACE_CONTAINER_HIGH, 0.80f * appear);
- Color hoverColor = applyAlpha(MD3Theme.PRIMARY_CONTAINER, 0.92f * appear);
- Color cardColor = blend(baseColor, hoverColor, hover);
- Color titleColor = blend(
- applyAlpha(MD3Theme.TEXT_PRIMARY, 0.95f * appear),
- applyAlpha(MD3Theme.ON_PRIMARY_CONTAINER, 0.98f * appear),
- hover
- );
- Color subColor = blend(
- applyAlpha(MD3Theme.TEXT_SECONDARY, 0.88f * appear),
- applyAlpha(MD3Theme.ON_PRIMARY_CONTAINER, 0.78f * appear),
- hover * 0.8f
- );
-
- Color badgeBase = entry.title.equals("Quit")
- ? applyAlpha(MD3Theme.TERTIARY_CONTAINER, 0.90f * appear)
- : applyAlpha(MD3Theme.SECONDARY_CONTAINER, 0.92f * appear);
- Color badgeHover = entry.title.equals("Quit")
- ? applyAlpha(MD3Theme.ERROR, 0.92f * appear)
- : applyAlpha(MD3Theme.PRIMARY, 0.95f * appear);
- Color badgeColor = blend(badgeBase, badgeHover, hover);
- Color badgeTextColor = entry.title.equals("Quit")
- ? applyAlpha(MD3Theme.ON_TERTIARY, 0.98f * appear)
- : applyAlpha(MD3Theme.ON_SECONDARY_CONTAINER, 0.98f * appear);
-
- shadowRenderer.addShadow(drawX, cardY, layout.buttonWidth, layout.buttonHeight, layout.buttonRadius, (10.0f + hover * 6.0f) * layout.scale, applyAlpha(MD3Theme.SHADOW, (0.52f + hover * 0.12f) * appear));
-
- roundRectRenderer.addRoundRect(drawX, cardY, layout.buttonWidth, layout.buttonHeight, layout.buttonRadius, cardColor);
-
- float badgeX = drawX + 10.0f * layout.scale;
- float badgeY = cardY + (layout.buttonHeight - layout.badgeSize) / 2.0f;
- roundRectRenderer.addRoundRect(badgeX, badgeY, layout.badgeSize, layout.badgeSize, layout.badgeRadius, badgeColor);
-
- float badgeScale = 0.62f * layout.scale;
- float badgeTextX = badgeX + (layout.badgeSize - textRenderer.getWidth(entry.badge, badgeScale, StaticFontLoader.DUCKSANS)) / 2.0f;
- float badgeTextY = badgeY + (layout.badgeSize - textRenderer.getHeight(badgeScale, StaticFontLoader.DUCKSANS)) / 2.0f - 1.0f * layout.scale;
- textRenderer.addText(entry.badge, badgeTextX, badgeTextY, badgeScale, badgeTextColor, StaticFontLoader.DUCKSANS);
-
- float textX = badgeX + layout.badgeSize + 11.0f * layout.scale;
- float titleScale = 0.74f * layout.scale;
- float subtitleScale = 0.54f * layout.scale;
- float titleY = cardY + 8.0f * layout.scale;
- float subtitleY = titleY + textRenderer.getLineHeight(titleScale, StaticFontLoader.DUCKSANS) + 3.5f * layout.scale;
-
- textRenderer.addText(entry.title, textX, titleY, titleScale, titleColor, StaticFontLoader.DUCKSANS);
- textRenderer.addText(entry.subtitle, textX, subtitleY, subtitleScale, subColor);
-
- String tip = entry.title.equals("Quit") ? "Close" : "Open";
- float tipScale = 0.50f * layout.scale;
- float tipWidth = textRenderer.getWidth(tip, tipScale);
- float tipX = drawX + layout.buttonWidth - tipWidth - 14.0f * layout.scale;
- float tipY = cardY + (layout.buttonHeight - textRenderer.getHeight(tipScale)) / 2.0f - 1.0f * layout.scale;
- textRenderer.addText(tip, tipX, tipY, tipScale, applyAlpha(MD3Theme.TEXT_MUTED, (0.86f + hover * 0.10f) * appear));
- }
-
- private static float easeOutCubic(float value) {
- float t = Mth.clamp(value, 0.0f, 1.0f);
- float inv = 1.0f - t;
- return 1.0f - inv * inv * inv;
- }
-
- private static Color applyAlpha(Color color, float alphaFactor) {
- float factor = Mth.clamp(alphaFactor, 0.0f, 1.0f);
- return new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.round(color.getAlpha() * factor));
- }
-
- private static Color blend(Color start, Color end, float delta) {
- float t = Mth.clamp(delta, 0.0f, 1.0f);
- int r = (int) (start.getRed() + (end.getRed() - start.getRed()) * t);
- int g = (int) (start.getGreen() + (end.getGreen() - start.getGreen()) * t);
- int b = (int) (start.getBlue() + (end.getBlue() - start.getBlue()) * t);
- int a = (int) (start.getAlpha() + (end.getAlpha() - start.getAlpha()) * t);
- return new Color(r, g, b, a);
- }
-
- @Override
- public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
- if (event.button() == 0) {
- for (MenuEntry entry : entries) {
- if (entry.isHovered(event.x(), event.y())) {
- entry.action.run();
- return true;
- }
- }
- }
- return super.mouseClicked(event, doubleClick);
- }
-
- @Override
- public void removed() {
- super.removed();
- if (renderTarget != null) {
- renderTarget.close();
- renderTarget = null;
- }
- if (videoStarted) {
- videoStarted = false;
- VideoUtil.stop();
- }
- }
-
- @Override
- public boolean shouldCloseOnEsc() {
- return false;
- }
-
- @Override
- public boolean isPauseScreen() {
- return false;
- }
-
- private static final class MenuEntry {
- private final String badge;
- private final String title;
- private final String subtitle;
- private final Runnable action;
-
- private float x;
- private float y;
- private float width;
- private float height;
- private float hoverProgress;
-
- private MenuEntry(String badge, String title, String subtitle, Runnable action) {
- this.badge = badge;
- this.title = title;
- this.subtitle = subtitle;
- this.action = action;
- }
-
- private void setBounds(float x, float y, float width, float height) {
- this.x = x;
- this.y = y;
- this.width = width;
- this.height = height;
- }
-
- private boolean isHovered(double mouseX, double mouseY) {
- return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
- }
- }
-
- private record Layout(float scale, float panelX, float panelY, float panelWidth, float panelHeight,
- float panelRadius, float padding, float buttonsX, float buttonsY, float buttonWidth,
- float buttonHeight, float buttonGap, float buttonRadius, float badgeSize, float badgeRadius) {
- private static Layout resolve(int width, int height, int entryCount) {
- float minEdgePadding = 10.0f;
- float baseScale = Mth.clamp(Math.min(width / 480.0f, height / 320.0f), 0.92f, 1.18f);
-
- float panelWidthAtScaleOne = 252.0f;
- float panelHeightAtScaleOne = 45.0f + entryCount * 40.0f + Math.max(0, entryCount - 1) * 8.0f + 24.0f;
- float availableWidth = Math.max(1.0f, width - minEdgePadding * 2.0f);
- float availableHeight = Math.max(1.0f, height - minEdgePadding * 2.0f);
-
- float fitScale = Math.min(1.0f, Math.min(availableWidth / panelWidthAtScaleOne, availableHeight / panelHeightAtScaleOne));
- float scale = Mth.clamp(baseScale * fitScale, 0.58f, 1.18f);
-
- float buttonWidth = 228.0f * scale;
- float buttonHeight = 40.0f * scale;
- float buttonGap = 8.0f * scale;
- float padding = 12.0f * scale;
- float headerHeight = 45.0f * scale;
- float panelWidth = buttonWidth + padding * 2.0f;
- float panelHeight = headerHeight + entryCount * buttonHeight + (entryCount - 1) * buttonGap + padding * 2.0f;
-
- float panelX = Mth.clamp(width * 0.115f, minEdgePadding, Math.max(minEdgePadding, width - panelWidth - minEdgePadding));
- float targetY = height * 0.62f - panelHeight / 2.0f;
- float panelY = Mth.clamp(targetY, minEdgePadding, Math.max(minEdgePadding, height - panelHeight - minEdgePadding));
- return new Layout(
- scale,
- panelX,
- panelY,
- panelWidth,
- panelHeight,
- 16.0f * scale,
- padding,
- panelX + padding,
- panelY + headerHeight,
- buttonWidth,
- buttonHeight,
- buttonGap,
- 10.0f * scale,
- 24.0f * scale,
- 7.0f * scale
- );
- }
- }
-
-}
+//package com.github.epsilon.gui.screen;
+//
+//import com.github.epsilon.Epsilon;
+//import com.github.epsilon.graphics.LuminRenderSystem;
+//import com.github.epsilon.graphics.renderers.RoundRectRenderer;
+//import com.github.epsilon.graphics.renderers.ShadowRenderer;
+//import com.github.epsilon.graphics.renderers.TextRenderer;
+//import com.github.epsilon.graphics.text.StaticFontLoader;
+//import com.github.epsilon.graphics.video.VideoManager;
+//import com.github.epsilon.graphics.video.VideoUtil;
+//import com.github.epsilon.gui.panel.MD3Theme;
+//import net.minecraft.client.gui.GuiGraphicsExtractor;
+//import net.minecraft.client.gui.screens.Screen;
+//import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
+//import net.minecraft.client.gui.screens.multiplayer.SafetyScreen;
+//import net.minecraft.client.gui.screens.options.OptionsScreen;
+//import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen;
+//import net.minecraft.client.input.MouseButtonEvent;
+//import net.minecraft.network.chat.Component;
+//import net.minecraft.resources.Identifier;
+//import net.minecraft.util.Mth;
+//import net.minecraft.util.Util;
+//
+//import java.awt.*;
+//import java.io.IOException;
+//import java.util.ArrayList;
+//import java.util.List;
+//
+//public class MainMenuScreen extends Screen {
+//
+// private static final float VIDEO_ZOOM = 1.08f;
+// private static final float VIDEO_PARALLAX_STRENGTH = 0.35f;
+//
+// public static final MainMenuScreen INSTANCE = new MainMenuScreen();
+//
+// private final ShadowRenderer shadowRenderer = new ShadowRenderer();
+// private final RoundRectRenderer roundRectRenderer = new RoundRectRenderer();
+// private final TextRenderer textRenderer = new TextRenderer();
+//
+// private final List entries = new ArrayList<>();
+//
+// private LuminRenderSystem.LuminRenderTarget renderTarget;
+//
+// private boolean videoStarted;
+// private long introStartMs;
+//
+// private MainMenuScreen() {
+// super(Component.literal("MainMenuScreen"));
+// entries.add(new MenuEntry("S", "Singleplayer", "Create and manage your worlds", () -> minecraft.setScreen(new SelectWorldScreen(this))));
+// entries.add(new MenuEntry("M", "Multiplayer", "Servers, friends and online play", () -> {
+// Screen screen = this.minecraft.options.skipMultiplayerWarning ? new JoinMultiplayerScreen(this) : new SafetyScreen(this);
+// this.minecraft.setScreen(screen);
+// }));
+// entries.add(new MenuEntry("O", "Options", "Video, controls and client settings", () -> minecraft.setScreen(new OptionsScreen(this, minecraft.options, false))));
+// entries.add(new MenuEntry("Q", "Quit", "Leave Epsilon and close the game", minecraft::stop));
+// }
+//
+// @Override
+// protected void init() {
+// super.init();
+// introStartMs = Util.getMillis();
+// for (MenuEntry entry : entries) {
+// entry.hoverProgress = 0.0f;
+// entry.setBounds(0.0f, 0.0f, 0.0f, 0.0f);
+// }
+// }
+//
+// @Override
+// public void added() {
+// super.added();
+//
+// if (videoStarted) {
+// return;
+// }
+//
+// try {
+// VideoManager.loadBackground(60);
+// videoStarted = true;
+// } catch (IOException e) {
+// Epsilon.LOGGER.error("MainMenu视频加载失败!", e);
+// }
+// }
+//
+// @Override
+// public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
+// Identifier videoTexture = videoStarted ? VideoUtil.getGuiTexture() : null;
+// if (videoTexture != null) {
+// int videoWidth = VideoUtil.getGuiTextureWidth();
+// int videoHeight = VideoUtil.getGuiTextureHeight();
+// if (videoWidth > 0 && videoHeight > 0) {
+// float screenAspect = (float) this.width / (float) this.height;
+// float videoAspect = (float) videoWidth / (float) videoHeight;
+//
+// float visibleWidth = 1.0f;
+// float visibleHeight = 1.0f;
+// if (videoAspect > screenAspect) {
+// visibleWidth = screenAspect / videoAspect;
+// } else {
+// visibleHeight = videoAspect / screenAspect;
+// }
+//
+// visibleWidth /= VIDEO_ZOOM;
+// visibleHeight /= VIDEO_ZOOM;
+//
+// float mouseNormX = this.width <= 0 ? 0.0f : Mth.clamp((mouseX / (float) this.width) * 2.0f - 1.0f, -1.0f, 1.0f);
+// float mouseNormY = this.height <= 0 ? 0.0f : Mth.clamp((mouseY / (float) this.height) * 2.0f - 1.0f, -1.0f, 1.0f);
+//
+// float uRange = 1.0f - visibleWidth;
+// float vRange = 1.0f - visibleHeight;
+// float u0 = Mth.clamp((uRange * 0.5f) - mouseNormX * uRange * VIDEO_PARALLAX_STRENGTH, 0.0f, uRange);
+// float v0 = Mth.clamp((vRange * 0.5f) - mouseNormY * vRange * VIDEO_PARALLAX_STRENGTH, 0.0f, vRange);
+// float u1 = u0 + visibleWidth;
+// float v1 = v0 + visibleHeight;
+//
+// graphics.blit(videoTexture, 0, 0, this.width, this.height, u0, u1, v0, v1);
+// return;
+// }
+// return;
+// }
+//
+// // 视频已经启动但当前帧尚未准备好时,回退到全景背景,避免黑屏。
+// this.minecraft.gameRenderer.getPanorama().extractRenderState(graphics, this.width, this.height, this.panoramaShouldSpin());
+// }
+//
+// @Override
+// public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
+// final var window = minecraft.getWindow();
+// if (renderTarget == null) {
+// renderTarget = LuminRenderSystem.LuminRenderTarget.create("main-menu", window.getWidth(), window.getHeight());
+// }
+//
+// renderTarget.clear();
+// renderTarget.resize(window.getWidth(), window.getHeight());
+// LuminRenderSystem.setActiveTarget(renderTarget);
+//
+// MD3Theme.syncFromSettings();
+// drawMenu(mouseX, mouseY);
+//
+// LuminRenderSystem.setActiveTarget(null);
+// graphics.blit(renderTarget.getIdentifier(), 0, 0, window.getGuiScaledWidth(), window.getGuiScaledHeight(), 0, 1, 1, 0);
+// }
+//
+// private void drawMenu(int mouseX, int mouseY) {
+// long now = Util.getMillis();
+// float introProgress = easeOutCubic(Mth.clamp((now - introStartMs) / 650.0f, 0.0f, 1.0f));
+// Layout layout = Layout.resolve(width, height, entries.size());
+//
+// Color panelShadow = applyAlpha(MD3Theme.SHADOW, 0.90f);
+// Color panelColor = applyAlpha(MD3Theme.SURFACE, 0.74f);
+// Color titleColor = applyAlpha(MD3Theme.TEXT_PRIMARY, 0.96f);
+// Color subtitleColor = applyAlpha(MD3Theme.TEXT_SECONDARY, 0.90f);
+//
+// shadowRenderer.addShadow(layout.panelX, layout.panelY, layout.panelWidth, layout.panelHeight, layout.panelRadius, 22.0f * layout.scale, panelShadow);
+// roundRectRenderer.addRoundRect(layout.panelX, layout.panelY, layout.panelWidth, layout.panelHeight, layout.panelRadius, panelColor);
+//
+// float titleScale = 1.04f * layout.scale;
+// float versionScale = 0.56f * layout.scale;
+// float titleY = layout.panelY + layout.padding + 2.0f * layout.scale;
+// float subtitleY = titleY + textRenderer.getLineHeight(titleScale, StaticFontLoader.DUCKSANS) + 2.0f * layout.scale;
+//
+// textRenderer.addText("Open Epsilon", layout.panelX + layout.padding, titleY, titleScale, titleColor, StaticFontLoader.DUCKSANS);
+// textRenderer.addText("Minecraft 26.1.2 | " + Epsilon.VERSION, layout.panelX + layout.padding, subtitleY, versionScale, subtitleColor);
+//
+// for (int i = 0; i < entries.size(); i++) {
+// renderEntry(entries.get(i), i, mouseX, mouseY, introProgress, layout);
+// }
+//
+// shadowRenderer.drawAndClear();
+// roundRectRenderer.drawAndClear();
+// textRenderer.drawAndClear();
+// }
+//
+// private void renderEntry(MenuEntry entry, int index, int mouseX, int mouseY, float introProgress, Layout layout) {
+// float staged = Mth.clamp((introProgress - index * 0.09f) / 0.60f, 0.0f, 1.0f);
+// float appear = easeOutCubic(staged);
+// if (appear <= 0.001f) {
+// entry.setBounds(0.0f, 0.0f, 0.0f, 0.0f);
+// return;
+// }
+//
+// float y = layout.buttonsY + index * (layout.buttonHeight + layout.buttonGap);
+// float slideX = (1.0f - appear) * (-18.0f * layout.scale);
+// float drawX = layout.buttonsX + slideX;
+//
+// entry.setBounds(drawX, y, layout.buttonWidth, layout.buttonHeight);
+//
+// boolean hovered = entry.isHovered(mouseX, mouseY);
+// entry.hoverProgress = Mth.lerp(hovered ? 0.24f : 0.16f, entry.hoverProgress, hovered ? 1.0f : 0.0f);
+//
+// float hover = entry.hoverProgress;
+// float cardLift = hover * 2.0f * layout.scale;
+// float cardY = y - cardLift;
+//
+// Color baseColor = applyAlpha(MD3Theme.SURFACE_CONTAINER_HIGH, 0.80f * appear);
+// Color hoverColor = applyAlpha(MD3Theme.PRIMARY_CONTAINER, 0.92f * appear);
+// Color cardColor = blend(baseColor, hoverColor, hover);
+// Color titleColor = blend(
+// applyAlpha(MD3Theme.TEXT_PRIMARY, 0.95f * appear),
+// applyAlpha(MD3Theme.ON_PRIMARY_CONTAINER, 0.98f * appear),
+// hover
+// );
+// Color subColor = blend(
+// applyAlpha(MD3Theme.TEXT_SECONDARY, 0.88f * appear),
+// applyAlpha(MD3Theme.ON_PRIMARY_CONTAINER, 0.78f * appear),
+// hover * 0.8f
+// );
+//
+// Color badgeBase = entry.title.equals("Quit")
+// ? applyAlpha(MD3Theme.TERTIARY_CONTAINER, 0.90f * appear)
+// : applyAlpha(MD3Theme.SECONDARY_CONTAINER, 0.92f * appear);
+// Color badgeHover = entry.title.equals("Quit")
+// ? applyAlpha(MD3Theme.ERROR, 0.92f * appear)
+// : applyAlpha(MD3Theme.PRIMARY, 0.95f * appear);
+// Color badgeColor = blend(badgeBase, badgeHover, hover);
+// Color badgeTextColor = entry.title.equals("Quit")
+// ? applyAlpha(MD3Theme.ON_TERTIARY, 0.98f * appear)
+// : applyAlpha(MD3Theme.ON_SECONDARY_CONTAINER, 0.98f * appear);
+//
+// shadowRenderer.addShadow(drawX, cardY, layout.buttonWidth, layout.buttonHeight, layout.buttonRadius, (10.0f + hover * 6.0f) * layout.scale, applyAlpha(MD3Theme.SHADOW, (0.52f + hover * 0.12f) * appear));
+//
+// roundRectRenderer.addRoundRect(drawX, cardY, layout.buttonWidth, layout.buttonHeight, layout.buttonRadius, cardColor);
+//
+// float badgeX = drawX + 10.0f * layout.scale;
+// float badgeY = cardY + (layout.buttonHeight - layout.badgeSize) / 2.0f;
+// roundRectRenderer.addRoundRect(badgeX, badgeY, layout.badgeSize, layout.badgeSize, layout.badgeRadius, badgeColor);
+//
+// float badgeScale = 0.62f * layout.scale;
+// float badgeTextX = badgeX + (layout.badgeSize - textRenderer.getWidth(entry.badge, badgeScale, StaticFontLoader.DUCKSANS)) / 2.0f;
+// float badgeTextY = badgeY + (layout.badgeSize - textRenderer.getHeight(badgeScale, StaticFontLoader.DUCKSANS)) / 2.0f - 1.0f * layout.scale;
+// textRenderer.addText(entry.badge, badgeTextX, badgeTextY, badgeScale, badgeTextColor, StaticFontLoader.DUCKSANS);
+//
+// float textX = badgeX + layout.badgeSize + 11.0f * layout.scale;
+// float titleScale = 0.74f * layout.scale;
+// float subtitleScale = 0.54f * layout.scale;
+// float titleY = cardY + 8.0f * layout.scale;
+// float subtitleY = titleY + textRenderer.getLineHeight(titleScale, StaticFontLoader.DUCKSANS) + 3.5f * layout.scale;
+//
+// textRenderer.addText(entry.title, textX, titleY, titleScale, titleColor, StaticFontLoader.DUCKSANS);
+// textRenderer.addText(entry.subtitle, textX, subtitleY, subtitleScale, subColor);
+//
+// String tip = entry.title.equals("Quit") ? "Close" : "Open";
+// float tipScale = 0.50f * layout.scale;
+// float tipWidth = textRenderer.getWidth(tip, tipScale);
+// float tipX = drawX + layout.buttonWidth - tipWidth - 14.0f * layout.scale;
+// float tipY = cardY + (layout.buttonHeight - textRenderer.getHeight(tipScale)) / 2.0f - 1.0f * layout.scale;
+// textRenderer.addText(tip, tipX, tipY, tipScale, applyAlpha(MD3Theme.TEXT_MUTED, (0.86f + hover * 0.10f) * appear));
+// }
+//
+// private static float easeOutCubic(float value) {
+// float t = Mth.clamp(value, 0.0f, 1.0f);
+// float inv = 1.0f - t;
+// return 1.0f - inv * inv * inv;
+// }
+//
+// private static Color applyAlpha(Color color, float alphaFactor) {
+// float factor = Mth.clamp(alphaFactor, 0.0f, 1.0f);
+// return new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.round(color.getAlpha() * factor));
+// }
+//
+// private static Color blend(Color start, Color end, float delta) {
+// float t = Mth.clamp(delta, 0.0f, 1.0f);
+// int r = (int) (start.getRed() + (end.getRed() - start.getRed()) * t);
+// int g = (int) (start.getGreen() + (end.getGreen() - start.getGreen()) * t);
+// int b = (int) (start.getBlue() + (end.getBlue() - start.getBlue()) * t);
+// int a = (int) (start.getAlpha() + (end.getAlpha() - start.getAlpha()) * t);
+// return new Color(r, g, b, a);
+// }
+//
+// @Override
+// public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
+// if (event.button() == 0) {
+// for (MenuEntry entry : entries) {
+// if (entry.isHovered(event.x(), event.y())) {
+// entry.action.run();
+// return true;
+// }
+// }
+// }
+// return super.mouseClicked(event, doubleClick);
+// }
+//
+// @Override
+// public void removed() {
+// super.removed();
+// if (renderTarget != null) {
+// renderTarget.close();
+// renderTarget = null;
+// }
+// if (videoStarted) {
+// videoStarted = false;
+// VideoUtil.stop();
+// }
+// }
+//
+// @Override
+// public boolean shouldCloseOnEsc() {
+// return false;
+// }
+//
+// @Override
+// public boolean isPauseScreen() {
+// return false;
+// }
+//
+// private static final class MenuEntry {
+// private final String badge;
+// private final String title;
+// private final String subtitle;
+// private final Runnable action;
+//
+// private float x;
+// private float y;
+// private float width;
+// private float height;
+// private float hoverProgress;
+//
+// private MenuEntry(String badge, String title, String subtitle, Runnable action) {
+// this.badge = badge;
+// this.title = title;
+// this.subtitle = subtitle;
+// this.action = action;
+// }
+//
+// private void setBounds(float x, float y, float width, float height) {
+// this.x = x;
+// this.y = y;
+// this.width = width;
+// this.height = height;
+// }
+//
+// private boolean isHovered(double mouseX, double mouseY) {
+// return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
+// }
+// }
+//
+// private record Layout(float scale, float panelX, float panelY, float panelWidth, float panelHeight,
+// float panelRadius, float padding, float buttonsX, float buttonsY, float buttonWidth,
+// float buttonHeight, float buttonGap, float buttonRadius, float badgeSize, float badgeRadius) {
+// private static Layout resolve(int width, int height, int entryCount) {
+// float minEdgePadding = 10.0f;
+// float baseScale = Mth.clamp(Math.min(width / 480.0f, height / 320.0f), 0.92f, 1.18f);
+//
+// float panelWidthAtScaleOne = 252.0f;
+// float panelHeightAtScaleOne = 45.0f + entryCount * 40.0f + Math.max(0, entryCount - 1) * 8.0f + 24.0f;
+// float availableWidth = Math.max(1.0f, width - minEdgePadding * 2.0f);
+// float availableHeight = Math.max(1.0f, height - minEdgePadding * 2.0f);
+//
+// float fitScale = Math.min(1.0f, Math.min(availableWidth / panelWidthAtScaleOne, availableHeight / panelHeightAtScaleOne));
+// float scale = Mth.clamp(baseScale * fitScale, 0.58f, 1.18f);
+//
+// float buttonWidth = 228.0f * scale;
+// float buttonHeight = 40.0f * scale;
+// float buttonGap = 8.0f * scale;
+// float padding = 12.0f * scale;
+// float headerHeight = 45.0f * scale;
+// float panelWidth = buttonWidth + padding * 2.0f;
+// float panelHeight = headerHeight + entryCount * buttonHeight + (entryCount - 1) * buttonGap + padding * 2.0f;
+//
+// float panelX = Mth.clamp(width * 0.115f, minEdgePadding, Math.max(minEdgePadding, width - panelWidth - minEdgePadding));
+// float targetY = height * 0.62f - panelHeight / 2.0f;
+// float panelY = Mth.clamp(targetY, minEdgePadding, Math.max(minEdgePadding, height - panelHeight - minEdgePadding));
+// return new Layout(
+// scale,
+// panelX,
+// panelY,
+// panelWidth,
+// panelHeight,
+// 16.0f * scale,
+// padding,
+// panelX + padding,
+// panelY + headerHeight,
+// buttonWidth,
+// buttonHeight,
+// buttonGap,
+// 10.0f * scale,
+// 24.0f * scale,
+// 7.0f * scale
+// );
+// }
+// }
+//
+//}
diff --git a/common/src/main/java/com/github/epsilon/mixins/MixinChatComponent.java b/common/src/main/java/com/github/epsilon/mixins/MixinChatComponent.java
index 74593633..d2b4a088 100644
--- a/common/src/main/java/com/github/epsilon/mixins/MixinChatComponent.java
+++ b/common/src/main/java/com/github/epsilon/mixins/MixinChatComponent.java
@@ -13,7 +13,7 @@
public class MixinChatComponent {
@ModifyVariable(method = "handleMessage", at = @At("HEAD"), argsOnly = true, ordinal = 0)
- private FormattedCharSequence sakura$animateClientPrefix(FormattedCharSequence message) {
+ private FormattedCharSequence onHandleMessage(FormattedCharSequence message) {
return ChatUtils.applyAnimatedPrefix(message);
}
diff --git a/common/src/main/java/com/github/epsilon/mixins/MixinTitleScreen.java b/common/src/main/java/com/github/epsilon/mixins/MixinTitleScreen.java
index 264d744a..8e7fafcb 100644
--- a/common/src/main/java/com/github/epsilon/mixins/MixinTitleScreen.java
+++ b/common/src/main/java/com/github/epsilon/mixins/MixinTitleScreen.java
@@ -1,8 +1,5 @@
package com.github.epsilon.mixins;
-import com.github.epsilon.gui.screen.MainMenuScreen;
-import com.github.epsilon.modules.impl.ClientSetting;
-import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.TitleScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -14,10 +11,10 @@ public class MixinTitleScreen {
@Inject(method = "init", at = @At("HEAD"), cancellable = true)
private void redirectToMainMenu(CallbackInfo ci) {
- if (ClientSetting.INSTANCE.useMainMenu.getValue()) {
- ci.cancel();
- Minecraft.getInstance().setScreen(MainMenuScreen.INSTANCE);
- }
+// if (ClientSetting.INSTANCE.useMainMenu.getValue()) {
+// ci.cancel();
+// Minecraft.getInstance().setScreen(MainMenuScreen.INSTANCE);
+// }
}
}
diff --git a/common/src/main/java/com/github/epsilon/modules/impl/ClientSetting.java b/common/src/main/java/com/github/epsilon/modules/impl/ClientSetting.java
index ee5d861c..43ed446d 100644
--- a/common/src/main/java/com/github/epsilon/modules/impl/ClientSetting.java
+++ b/common/src/main/java/com/github/epsilon/modules/impl/ClientSetting.java
@@ -58,7 +58,7 @@ public enum ThemeMode {
public final BoolSetting customTitle = boolSetting("Custom Title", true, _ -> mc.updateTitle());
- public final BoolSetting useMainMenu = boolSetting("Use MainMenu", true);
+ //public final BoolSetting useMainMenu = boolSetting("Use MainMenu", true);
public final BoolSetting soundNotify = boolSetting("Sound Notify", true);
diff --git a/common/src/main/java/com/github/epsilon/utils/BytecodeInjector.java b/common/src/main/java/com/github/epsilon/utils/BytecodeInjector.java
new file mode 100644
index 00000000..c19560dc
--- /dev/null
+++ b/common/src/main/java/com/github/epsilon/utils/BytecodeInjector.java
@@ -0,0 +1,262 @@
+package com.github.epsilon.utils;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.MethodNode;
+
+
+/**
+ * 免写ASM的方法头字节码注入工具。
+ *
+ * 设计思路
+ * 不想手写 ASM 来生成字节码?只需将自定义逻辑写在一个 Dummy Class 的普通 Java 方法里,
+ * 让它随项目一起编译。运行时,通过 {@link #injectMethodHead} 把这个 Dummy 方法的字节码
+ * 提取出来,注入到目标方法的最开头。
+ *
+ * 为什么可行
+ * 因为 Dummy 方法的形参和 Target 方法完全一致(Fabric Mixin 天然保证),局部变量表
+ * 的 slot 映射是 1:1 的,不需要任何重映射。
+ *
+ * 用法示例
+ * {@code
+ * // 1. 编写你的逻辑(Dummy Class 与方法)
+ * public class DummyLogic {
+ * // 形参必须和目标方法一模一样(Mixin handler 天然满足)
+ * public void dummyMethod(Entity entity, int x, int y, int z) {
+ * System.out.println("Before original method!");
+ * entity.setPos(x + 1, y, z); // 你的任意逻辑
+ * // 注意:不要写 return!(方法头注入不应提前返回)
+ * }
+ * }
+ *
+ * // 2. 运行时注入
+ * byte[] targetBytes = getClassBytes("net/minecraft/world/entity/Entity");
+ * byte[] dummyBytes = getClassBytes("com/example/DummyLogic");
+ * byte[] modified = BytecodeInjector.injectMethodHead(
+ * targetBytes, dummyBytes,
+ * "methodName", "(Lnet/minecraft/world/entity/Entity;III)V", // target
+ * "dummyMethod", "(Lnet/minecraft/world/entity/Entity;III)V" // dummy
+ * );
+ * redefineClass("net.minecraft.world.entity.Entity", modified);
+ * }
+ *
+ * @see ASM 9.x
+ * @see Sponge Mixin
+ */
+public final class BytecodeInjector {
+
+ private BytecodeInjector() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+
+ // ---- 返回值类型 ----
+
+ /**
+ * 注入结果,包含修改后的字节码和诊断信息。
+ */
+ public static final class InjectResult {
+ public final byte[] classBytes;
+ public final int instructionsInjected;
+ public final int maxStackBefore;
+ public final int maxStackAfter;
+ public final int maxLocalsBefore;
+ public final int maxLocalsAfter;
+
+ InjectResult(byte[] classBytes, int instructionsInjected,
+ int maxStackBefore, int maxStackAfter,
+ int maxLocalsBefore, int maxLocalsAfter) {
+ this.classBytes = classBytes;
+ this.instructionsInjected = instructionsInjected;
+ this.maxStackBefore = maxStackBefore;
+ this.maxStackAfter = maxStackAfter;
+ this.maxLocalsBefore = maxLocalsBefore;
+ this.maxLocalsAfter = maxLocalsAfter;
+ }
+ }
+
+ // ---- 公开 API ----
+
+ /**
+ * 将 Dummy 类中指定方法的方法体注入到目标类同名同参方法的方法头(最开头)。
+ *
+ * @param targetClassBytes 目标类的 raw bytes
+ * @param dummyClassBytes Dummy 类的 raw bytes(包含要注入的逻辑)
+ * @param targetMethodName 目标方法名
+ * @param targetMethodDesc 目标方法描述符
+ * @param dummyMethodName Dummy 方法名(要提取的方法)
+ * @param dummyMethodDesc Dummy 方法描述符(必须与 target 一致)
+ * @return 注入结果,包含修改后的字节码
+ * @throws IllegalArgumentException 如果方法不存在或参数签名不匹配
+ */
+ public static InjectResult injectMethodHead(
+ byte[] targetClassBytes,
+ byte[] dummyClassBytes,
+ String targetMethodName,
+ String targetMethodDesc,
+ String dummyMethodName,
+ String dummyMethodDesc) {
+
+ // 1. 解析两个类
+ ClassNode targetNode = parseClass(targetClassBytes);
+ ClassNode dummyNode = parseClass(dummyClassBytes);
+
+ // 2. 找到各自的方法
+ MethodNode targetMethod = findMethod(targetNode, targetMethodName, targetMethodDesc);
+ MethodNode dummyMethod = findMethod(dummyNode, dummyMethodName, dummyMethodDesc);
+
+ if (targetMethod == null) {
+ throw new IllegalArgumentException(
+ "Target method not found: " + targetMethodName + targetMethodDesc
+ + " in " + targetNode.name);
+ }
+ if (dummyMethod == null) {
+ throw new IllegalArgumentException(
+ "Dummy method not found: " + dummyMethodName + dummyMethodDesc
+ + " in " + dummyNode.name);
+ }
+
+ // 3. 验证参数签名兼容
+ validateParamCompatibility(targetMethod, dummyMethod);
+
+ // 4. 记录注入前的状态
+ int maxStackBefore = targetMethod.maxStack;
+ int maxLocalsBefore = targetMethod.maxLocals;
+
+ // 5. 移除 dummy 方法末尾的所有 RETURN 族指令
+ // 方法头注入不应包含 return,否则原方法体永远得不到执行
+ stripReturnInstructions(dummyMethod);
+
+ // 6. 如果有 try-catch 块,迁移到目标方法
+ // (标签会随 InsnList.insert() 一起自然迁移)
+ if (dummyMethod.tryCatchBlocks != null && !dummyMethod.tryCatchBlocks.isEmpty()) {
+ targetMethod.tryCatchBlocks.addAll(dummyMethod.tryCatchBlocks);
+ dummyMethod.tryCatchBlocks.clear();
+ }
+
+ // 7. 核心操作:将 dummy 的全部指令插入到 target 方法头部
+ // InsnList.insert(InsnList) 会把整个链表插入到 firstInsn 之前
+ // 并调用 removeAll(false) 清空源链表(指令被"移动"而非复制)
+ // 标签 LabelNode 随指令一起迁移,因此不需要 cloneLabels
+ int instructionsInjected = dummyMethod.instructions.size();
+ targetMethod.instructions.insert(dummyMethod.instructions);
+
+ // 8. 调整 maxStack 和 maxLocals(取较大值)
+ targetMethod.maxStack = Math.max(targetMethod.maxStack, dummyMethod.maxStack);
+ targetMethod.maxLocals = Math.max(targetMethod.maxLocals, dummyMethod.maxLocals);
+
+ // 9. 写出字节码(COMPUTE_FRAMES 让 ASM 自动重算栈帧)
+ byte[] result = writeClass(targetNode);
+
+ return new InjectResult(
+ result, instructionsInjected,
+ maxStackBefore, targetMethod.maxStack,
+ maxLocalsBefore, targetMethod.maxLocals
+ );
+ }
+
+ // ---- 内部工具方法 ----
+
+ /**
+ * 用 ASM ClassReader 将 raw bytes 解析为 ClassNode 树。
+ * 使用 EXPAND_FRAMES 确保栈帧信息完整展开。
+ */
+ private static ClassNode parseClass(byte[] bytes) {
+ ClassReader reader = new ClassReader(bytes);
+ ClassNode node = new ClassNode(Opcodes.ASM9);
+ reader.accept(node, ClassReader.EXPAND_FRAMES);
+ return node;
+ }
+
+ /**
+ * 在 ClassNode 中按 name + descriptor 查找方法。
+ */
+ private static MethodNode findMethod(ClassNode classNode, String name, String desc) {
+ for (MethodNode method : classNode.methods) {
+ if (method.name.equals(name) && method.desc.equals(desc)) {
+ return method;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 验证目标方法和 Dummy 方法的参数兼容性。
+ * 因为局部变量表要对齐,所以参数类型必须严格一致。
+ */
+ private static void validateParamCompatibility(MethodNode target, MethodNode dummy) {
+ Type[] targetArgs = Type.getArgumentTypes(target.desc);
+ Type[] dummyArgs = Type.getArgumentTypes(dummy.desc);
+
+ if (targetArgs.length != dummyArgs.length) {
+ throw new IllegalArgumentException(String.format(
+ "Parameter count mismatch: target has %d, dummy has %d. "
+ + "Methods must have identical parameter lists for LVT alignment.",
+ targetArgs.length, dummyArgs.length));
+ }
+
+ for (int i = 0; i < targetArgs.length; i++) {
+ if (!targetArgs[i].equals(dummyArgs[i])) {
+ throw new IllegalArgumentException(String.format(
+ "Parameter type mismatch at index %d: target=%s, dummy=%s. "
+ + "All parameter types must be identical for LVT alignment.",
+ i, targetArgs[i].getClassName(), dummyArgs[i].getClassName()));
+ }
+ }
+ }
+
+ /**
+ * 从方法末尾剥离所有 RETURN 族指令(IRETURN, LRETURN, FRETURN, DRETURN, ARETURN, RETURN)。
+ *
+ * 方法头注入的场景下,注入的代码不应该包含 return。否则会导致原方法体
+ * 永远执行不到。此方法从后往前遍历,删除所有 return 族指令,直到遇到
+ * 非 return 的指令为止。
+ *
+ * @return 被移除的指令数量
+ */
+ private static int stripReturnInstructions(MethodNode method) {
+ int removed = 0;
+ InsnList insns = method.instructions;
+ AbstractInsnNode insn = insns.getLast();
+
+ while (insn != null && isReturnOpcode(insn.getOpcode())) {
+ AbstractInsnNode prev = insn.getPrevious();
+ insns.remove(insn);
+ removed++;
+ insn = prev;
+ }
+
+ return removed;
+ }
+
+ /**
+ * 判断给定 opcode 是否为 RETURN 族指令。
+ * 覆盖范围:IRETURN(172) ~ RETURN(177)
+ */
+ private static boolean isReturnOpcode(int opcode) {
+ return opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN;
+ }
+
+ /**
+ * 用 ClassWriter 将 ClassNode 写回 byte[]。
+ * 使用 COMPUTE_FRAMES 自动重算栈帧,这比 COMPUTE_MAXS 更准确
+ * 但要求 classpath 上有所有引用到的类(Minecraft 环境下通常满足)。
+ * 如果帧计算失败,回退到 COMPUTE_MAXS。
+ */
+ private static byte[] writeClass(ClassNode classNode) {
+ try {
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+ classNode.accept(writer);
+ return writer.toByteArray();
+ } catch (Exception e) {
+ // 帧计算失败(通常是 classpath 不完整),回退到仅计算 maxs
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+ classNode.accept(writer);
+ return writer.toByteArray();
+ }
+ }
+}
diff --git a/common/src/main/java/com/github/epsilon/utils/DummyLogicExample.java b/common/src/main/java/com/github/epsilon/utils/DummyLogicExample.java
new file mode 100644
index 00000000..81ef07fc
--- /dev/null
+++ b/common/src/main/java/com/github/epsilon/utils/DummyLogicExample.java
@@ -0,0 +1,275 @@
+package com.github.epsilon.utils;
+
+import com.github.epsilon.events.bus.EventBus;
+
+/**
+ *
Dummy 类编写指南
+ *
+ * 这是配合 {@link BytecodeInjector} 使用的 Dummy Class。你只需在其中写一个普通 Java 方法,
+ * 方法签名必须和目标方法完全一致(因为 Fabric Mixin handler 天然保证参数签名相同)。
+ *
+ *
关键约束
+ *
+ * - 方法签名(参数类型+数量+返回值)必须与目标方法完全一致 —— 局部变量表要对齐
+ * - 方法体内 不要写 return 语句 —— 你的代码运行完后应 fall-through 到原方法体
+ * - 可以使用方法参数(this + 全部形参),slot 映射天然一致
+ * - 可以声明局部变量、调用任何方法、访问字段
+ * - ★ 修改形参 = 修改原方法看到的参数值(字节码层面 slot 共享)
+ *
+ *
+ * 形参转发原理(字节码层面)
+ * JVM 方法参数存储在局部变量表的 slot 中。因为 Dummy 和 Target 签名相同,
+ * 它们的 slot 布局完全一致。当你写 {@code x = newValue} 时,javac 生成
+ * {@code ISTORE },直接覆盖原方法的参数槽位。原方法体后续的
+ * {@code ILOAD } 就会读到修改后的值。
+ *
+ * 用法
+ * 编译后,运行时获取本类的 raw bytes,和目标的 raw bytes 一起交给
+ * {@link BytecodeInjector#injectMethodHead}。
+ *
+ * {@code
+ * // 获取 raw bytes
+ * byte[] dummyBytes = getClassBytes("com.github.epsilon.utils.DummyLogicExample");
+ * byte[] targetBytes = getClassBytes("net/minecraft/world/entity/Entity");
+ *
+ * // 注入
+ * BytecodeInjector.InjectResult result = BytecodeInjector.injectMethodHead(
+ * targetBytes, dummyBytes,
+ * "tick", "()V", // 目标方法
+ * "injectedLogic", "()V" // Dummy 方法
+ * );
+ * redefineClass(Entity.class, result.classBytes);
+ * }
+ *
+ * @see BytecodeInjector
+ * @see MethodInterceptEvent
+ */
+@SuppressWarnings("unused")
+public final class DummyLogicExample {
+
+ private DummyLogicExample() {
+ }
+
+ // ============================================================
+ // 示例 1:注入到无参方法头 (如 Entity.tick()V)
+ // ============================================================
+ // 目标方法签名: public void tick()
+ // Dummy 签名: public void injectedLogic()
+ //
+ // 注意:注入到实例方法时,dummy 方法也必须是实例方法(非 static),
+ // 这样 slot 0 的 "this" 引用才会对齐。
+
+ /**
+ * 注入到 Entity.tick()V 的方法头。
+ * 这里写你想在 tick() 开头执行的任意逻辑。
+ */
+ public void injectedLogic() {
+ // 示例:打印一条日志
+ System.out.println("[Epsilon] Entity.tick() called!");
+
+ // 可以访问 this(slot 0 指向目标 Entity 实例)
+ // ((Entity)(Object)this).setCustomNameVisible(true);
+
+ // 不要写 return!方法体会 fall-through 到原来的 tick() 逻辑
+ }
+
+ // ============================================================
+ // 示例 2:注入到有参方法头 (如 Entity.setPos(DDD)V)
+ // ============================================================
+ // 目标方法签名: public void setPos(double x, double y, double z)
+ // Dummy 签名: public void injectedSetPos(double x, double y, double z)
+
+ /**
+ * 注入到 Entity.setPos(DDD)V 的方法头。
+ * 参数列表必须和目标方法一致:double x, double y, double z
+ */
+ public void injectedSetPos(double x, double y, double z) {
+ // 可以读取和修改参数
+ System.out.printf("[Epsilon] setPos called: x=%.2f, y=%.2f, z=%.2f%n", x, y, z);
+
+ // 可以写条件逻辑
+ if (y < 0) {
+ System.out.println("[Epsilon] Player is below y=0!");
+ }
+
+ // 注意:修改参数的值不会影响调用者
+ // (Java 是值传递,但你可以通过修改对象字段来产生影响)
+ // 不要写 return!
+ }
+
+ // ============================================================
+ // 示例 3:注入到 static 方法头(slot 0 就是第一个参数)
+ // ============================================================
+ public static void injectedStatic(int a) {
+ System.out.println("[Epsilon] Static called with a=" + a);
+ a = a * 2; // ★ 对 static 方法同样可以直接修改形参
+ // slot 0 = 参数a(因为没有 this),所以 ISTORE 0
+ }
+
+ // ============================================================
+ // 示例 4:有返回值的方法(不写 return,原方法决定返回值)
+ // ============================================================
+ @SuppressWarnings("SameReturnValue")
+ public boolean injectedIsAlive() {
+ System.out.println("[Epsilon] isAlive() check intercepted!");
+ return true; // BytecodeInjector 会自动剥离 RETURN
+ }
+
+ // ============================================================
+ // 示例 5:★ 形参转发 + 回写(配合 MethodInterceptEvent)
+ // ============================================================
+ // 核心演示:转发参数 → EventBus → 监听器修改 → 写回形参 → 原方法看到修改
+ //
+ // 目标方法签名: public void tick(int ticks)
+ // Dummy 签名: public void interceptTick(int ticks)
+ //
+ // 关键步骤:
+ // 1. 用形参构造 MethodInterceptEvent(从 slot 读取参数)
+ // 2. 通过 EventBus 分发给所有监听器
+ // 3. ★ 把 event 中可能被修改的值写回形参(写回同一 slot)
+ // 4. 检查 isCancelled() 决定是否跳过原方法体
+
+ /**
+ * 拦截 Entity 或某类的 tick(int) 方法,将参数转发给监听器处理。
+ */
+ public void interceptTick(int ticks) {
+ // 1. 打包参数
+ MethodInterceptEvent event = new MethodInterceptEvent(ticks);
+
+ // 2. 发送事件(监听器可以自由修改 event 中的值)
+ EventBus.INSTANCE.post(event);
+
+ // 3. ★ 把修改后的值写回形参!
+ // 编译器生成:ILOAD , INVOKEVIRTUAL getInt, ISTORE
+ // 这个 ISTORE 直接写入原方法的参数 slot,原方法体随后 ILOAD 就会读到新值
+ ticks = event.getInt(0);
+
+ // 4. 如果监听器取消了事件,跳过原方法体
+ // 注意:这里不能用 return,因为 BytecodeInjector 会剥离 RETURN
+ // 解决办法:用 if-else 包裹原方法调用
+ // 更简单的:如果不取消,正常 fall-through;如果取消了,写一个标志位
+ // 或者:直接在 event 中设置一个 dirty flag,让后续模块检查
+ }
+
+ // ============================================================
+ // 示例 6:★ 直接修改形参(无需 EventBus,最简单)
+ // ============================================================
+ // 如果你不需要事件系统,只想在方法执行前修改参数,直接赋值即可。
+ // 因为编译后的 ISTORE 操作的就是原方法的 slot。
+ //
+ // 目标方法签名: public void setMotion(double x, double y, double z)
+ // Dummy 签名: public void modifyMotion(double x, double y, double z)
+
+ /**
+ * 直接修改方法参数(无需事件)。
+ * 修改后原方法体看到的 x, y, z 就是新值。
+ */
+ public void modifyMotion(double x, double y, double z) {
+ // 直接修改形参 —— ISTORE 到 slot 1/2/3/4/5
+ // 原方法体后续 ILOAD/DLOAD 会读到修改后的值
+ if (y < 0.0) {
+ y = 0.0; // 把负的 y 归零
+ }
+ x *= 1.5; // 加速 x
+ z *= 1.5; // 加速 z
+ }
+
+ // ============================================================
+ // 示例 7:★ 形参转发(多类型混合)
+ // ============================================================
+ // 目标方法签名: public void handleInput(int key, boolean pressed, String context)
+ // Dummy 签名: public void interceptInput(int key, boolean pressed, String context)
+
+ /**
+ * 多类型参数转发+回写。注意每个参数都要写回。
+ */
+ public void interceptInput(int key, boolean pressed, String context) {
+ // 1. 打包(注意 boolean 会被自动装箱为 Boolean)
+ MethodInterceptEvent event = new MethodInterceptEvent(key, pressed, context);
+
+ // 2. 分发给监听器
+ EventBus.INSTANCE.post(event);
+
+ // 3. ★ 写回每个参数(缺一不可!)
+ key = event.getInt(0);
+ pressed = event.getBoolean(1);
+ context = event.getObject(2);
+
+ // 如果监听器把 key 改成了 -1(表示拦截),后续模块可以据此判断
+ }
+
+ // ============================================================
+ // 示例 8:★ Mixin 变换模板(配合 MixinRuntimeLauncher)
+ // ============================================================
+ // 当你需要 @Inject、@Overwrite、@ModifyVariable 等 Mixin 注解能力时,
+ // 使用 MixinRuntimeLauncher 代替 BytecodeInjector。
+ //
+ // 前提:你的 Dummy 类必须编译为正常的 @Mixin 类(有 @Mixin 注解,
+ // 方法上有 @Inject/@Overwrite 等注解)。然后用 MixinRuntimeLauncher
+ // 在运行时加载它的 raw bytes 并触发完整的 Mixin 变换管线。
+ //
+ // 使用示例(在你的 Mixin 插件中):
+ // {@code
+ // // 1. 获取目标类和模板 Mixin 的 raw bytes
+ // byte[] targetBytes = YourRuntimeClassProvider.getBytes("net.minecraft.world.entity.Entity");
+ // byte[] mixinBytes = YourRuntimeClassProvider.getBytes("com.github.epsilon.mixins.MixinEntity");
+ //
+ // // 2. 一步调用:启动 Mixin 管线
+ // byte[] transformed = MixinRuntimeLauncher.applyMixin(
+ // "net.minecraft.world.entity.Entity", // 目标类全限定名
+ // targetBytes, // 目标类 raw bytes
+ // mixinBytes // Mixin 类 raw bytes
+ // );
+ //
+ // // 3. 重定义类
+ // YourClassRedefiner.redefine("net.minecraft.world.entity.Entity", transformed);
+ // }
+ //
+ // 和 BytecodeInjector 的区别:
+ //
+ // | 特性 | BytecodeInjector | MixinRuntimeLauncher |
+ //
+ // | 方法头注入 | ✓ (InsnList.insert) | ✓ (通过 @Inject(at=@At("HEAD"))) |
+ //
+ //
+ // | @Inject 任意位置 | ✗ | ✓ |
+ //
+ //
+ // | @Overwrite | ✗ | ✓ |
+ //
+ //
+ // | @ModifyVariable | ✗ | ✓ |
+ //
+ //
+ // | @ModifyArg | ✗ | ✓ |
+ //
+ //
+ // | @Redirect | ✗ | ✓ |
+ //
+ //
+ // | 描述符重映射 | ✗ (需手动) | ✓ (自动) |
+ //
+ //
+ // | @Shadow 字段/方法 | ✗ | ✓ |
+ //
+ //
+ // | CallbackInfo | ✗ | ✓ |
+ //
+ //
+ // | 简单直接 | ✓✓✓ | ✓ |
+ //
+ //
+
+ // ============================================================
+ // 示例 9:★ 混合模式 —— BytecodeInjector 参数转发 + MixinRuntimeLauncher 管线
+ // ============================================================
+ // 两种工具可以组合使用:
+ // 1. 先用 BytecodeInjector 注入自定义方法头(形参转发 + EventBus)
+ // 2. 再用 MixinRuntimeLauncher 启动完整 Mixin 管线(@Inject 等)
+ //
+ // 这让你同时拥有"免写 ASM"和"Mixin 注解全能力"。
+ //
+ // 注意:MixinRuntimeLauncher 要求 Mixin 框架已初始化。
+ // 在 Fabric/NeoForge 的 Minecraft 环境中,默认在 PREINIT 阶段完成。
+ // 你可以在 INIT 或之后的阶段安全调用 applyMixin()。
+}
diff --git a/common/src/main/java/com/github/epsilon/utils/MethodInterceptEvent.java b/common/src/main/java/com/github/epsilon/utils/MethodInterceptEvent.java
new file mode 100644
index 00000000..c64e755f
--- /dev/null
+++ b/common/src/main/java/com/github/epsilon/utils/MethodInterceptEvent.java
@@ -0,0 +1,124 @@
+package com.github.epsilon.utils;
+
+import com.github.epsilon.events.Cancellable;
+
+/**
+ * 方法拦截事件 —— 配合 {@link BytecodeInjector} 实现形参转发+回写。
+ *
+ * 设计思路
+ * 运行时,Dummy 方法的字节码被注入到目标方法头。因为两个方法的参数签名一致,
+ * 它们在 JVM 局部变量表中共享完全相同的 slot。这意味着:在 Dummy 方法里对形参的
+ * 重新赋值会直接修改原方法看到的参数值。
+ *
+ * 字节码原理
+ * 假设目标方法和 Dummy 方法签名都是 {@code (IDLjava/lang/String;)V}:
+ *
+ * JVM 局部变量表(共享):
+ * slot 0 → this
+ * slot 1 → int x
+ * slot 2-3 → double y
+ * slot 4 → String z
+ *
+ * Dummy 方法体被注入时执行:
+ * x = newValue; → ISTORE 1 (写入 slot 1)
+ *
+ * 原方法体执行时:
+ * ILOAD 1 → 读到的是修改后的值!
+ *
+ *
+ * 用法示例
+ * {@code
+ * // Dummy 类中
+ * public void dummyMethod(int x, double y, String z) {
+ * MethodInterceptEvent event = new MethodInterceptEvent(x, y, z);
+ * EventBus.INSTANCE.post(event);
+ *
+ * // ★ 关键:必须把修改后的值写回形参!
+ * // 因为编译器生成的 ISTORE/DSTORE/ASTORE 操作的正是
+ * // 原方法的局部变量槽位
+ * x = event.getInt(0);
+ * y = event.getDouble(1);
+ * z = (String) event.getObject(2);
+ * }
+ *
+ * // 监听器中
+ * EventBus.INSTANCE.register(MethodInterceptEvent.class, event -> {
+ * event.setInt(0, 42); // 修改 int 参数
+ * event.cancel(); // 可选:取消原方法执行
+ * });
+ * }
+ *
+ * @see BytecodeInjector
+ */
+public class MethodInterceptEvent extends Cancellable {
+
+ private final Object[] args;
+
+ /**
+ * @param args 方法的所有参数(按声明顺序)
+ */
+ public MethodInterceptEvent(Object... args) {
+ this.args = args;
+ }
+
+ // ---- 类型安全的读写 ----
+
+ public int getInt(int index) {
+ return (int) args[index];
+ }
+
+ public void setInt(int index, int value) {
+ args[index] = value;
+ }
+
+ public long getLong(int index) {
+ return (long) args[index];
+ }
+
+ public void setLong(int index, long value) {
+ args[index] = value;
+ }
+
+ public float getFloat(int index) {
+ return (float) args[index];
+ }
+
+ public void setFloat(int index, float value) {
+ args[index] = value;
+ }
+
+ public double getDouble(int index) {
+ return (double) args[index];
+ }
+
+ public void setDouble(int index, double value) {
+ args[index] = value;
+ }
+
+ public boolean getBoolean(int index) {
+ return (boolean) args[index];
+ }
+
+ public void setBoolean(int index, boolean value) {
+ args[index] = value;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getObject(int index) {
+ return (T) args[index];
+ }
+
+ public void setObject(int index, Object value) {
+ args[index] = value;
+ }
+
+ /** 参数个数 */
+ public int getArgCount() {
+ return args.length;
+ }
+
+ /** 直接访问内部数组(谨慎使用) */
+ public Object[] getArgs() {
+ return args;
+ }
+}
diff --git a/common/src/main/java/com/github/epsilon/utils/MixinMappingTable.java b/common/src/main/java/com/github/epsilon/utils/MixinMappingTable.java
new file mode 100644
index 00000000..56b9e963
--- /dev/null
+++ b/common/src/main/java/com/github/epsilon/utils/MixinMappingTable.java
@@ -0,0 +1,261 @@
+package com.github.epsilon.utils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Mixin → Target Minecraft 类的完整映射表。
+ *
+ * 用法
+ * {@code
+ * for (var entry : MixinMappingTable.COMMON_MIXINS.entrySet()) {
+ * String mixinInternalName = entry.getKey();
+ * String targetInternalName = entry.getValue();
+ *
+ * byte[] mixinBytes = getClassBytes(mixinInternalName); // 你的 bytes 提供器
+ * byte[] targetBytes = getClassBytes(targetInternalName);
+ *
+ * byte[] transformed = MixinRuntimeLauncher.applyMixin(
+ * targetInternalName.replace('/', '.'),
+ * targetBytes,
+ * mixinBytes
+ * );
+ * redefineClass(targetInternalName.replace('/', '.'), transformed);
+ * }
+ * }
+ *
+ * 说明
+ *
+ * - Key = Mixin 类 internal name(斜杠分隔)
+ * - Value = Target Minecraft 类 internal name(斜杠分隔)
+ * - 顺序与 {@code epsilon.mixins.json} 注册顺序一致
+ * - Accessor 接口(I*)只提供 getter,不含业务逻辑注入,可以不热重定义
+ * - Fabric/NeoForge 的 MixinMinecraft 和 MixinGuiRenderer 是 loader 专用,热重定义时用 COMMON_MIXINS 即可
+ *
+ *
+ * @see MixinRuntimeLauncher
+ * @see BytecodeInjector
+ */
+public final class MixinMappingTable {
+
+ private MixinMappingTable() {
+ }
+
+ /**
+ * Common 模块的 Mixin → Target 完整映射(43 条)。
+ * 使用 LinkedHashMap 保持注册顺序。
+ */
+ public static final Map COMMON_MIXINS = new LinkedHashMap<>();
+
+ static {
+ // === Accessor 接口(5 个)===
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/IAbstractContainerScreen",
+ "net/minecraft/client/gui/screens/inventory/AbstractContainerScreen"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/IMinecraft",
+ "net/minecraft/client/Minecraft"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/IReloadState",
+ "net/minecraft/client/ResourceLoadStateTracker$ReloadState"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/IResourceLoadStateTracker",
+ "net/minecraft/client/ResourceLoadStateTracker"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/IServerboundMovePlayerPacket",
+ "net/minecraft/network/protocol/game/ServerboundMovePlayerPacket"
+ );
+
+ // === 渲染相关 Mixin(11 个)===
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinAvatarRenderer",
+ "net/minecraft/client/renderer/entity/player/AvatarRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinBlockCollisions",
+ "net/minecraft/world/level/BlockCollisions"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinCamera",
+ "net/minecraft/client/Camera"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinChatComponent",
+ "net/minecraft/client/gui/components/ChatComponent" // targets inner: DrawingFocusedGraphicsAccess, DrawingBackgroundGraphicsAccess
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinEntityRenderer",
+ "net/minecraft/client/renderer/entity/LivingEntityRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinGameRenderer",
+ "net/minecraft/client/renderer/GameRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinHumanoidMobRenderer",
+ "net/minecraft/client/renderer/entity/HumanoidMobRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinItemInHandRenderer",
+ "net/minecraft/client/renderer/ItemInHandRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinLevelRenderer",
+ "net/minecraft/client/renderer/LevelRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinLivingEntityRenderer",
+ "net/minecraft/client/renderer/entity/LivingEntityRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinParticleManager",
+ "net/minecraft/client/particle/ParticleEngine"
+ );
+
+ // === 客户端核心 Mixin(11 个)===
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinClientLevel",
+ "net/minecraft/client/multiplayer/ClientLevel"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinClientPacketListener",
+ "net/minecraft/client/multiplayer/ClientPacketListener"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinConnection",
+ "net/minecraft/network/Connection"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinGlDebug",
+ "com/mojang/blaze3d/opengl/GlDebug"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinGui",
+ "net/minecraft/client/gui/Gui"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinGuiRenderer",
+ "net/minecraft/client/gui/render/GuiRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinLightmap",
+ "net/minecraft/client/renderer/Lightmap"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinMinecraft",
+ "net/minecraft/client/Minecraft"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinMouseHandler",
+ "net/minecraft/client/MouseHandler"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinScreenEffectRenderer",
+ "net/minecraft/client/renderer/ScreenEffectRenderer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinWindow",
+ "com/mojang/blaze3d/platform/Window"
+ );
+
+ // === 实体/玩家 Mixin(9 个)===
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinEntity",
+ "net/minecraft/world/entity/Entity"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinFireworkRocketEntity",
+ "net/minecraft/world/entity/projectile/FireworkRocketEntity"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinKeyboardInput",
+ "net/minecraft/client/player/KeyboardInput"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinLivingEntity",
+ "net/minecraft/world/entity/LivingEntity"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinLocalPlayer",
+ "net/minecraft/client/player/LocalPlayer"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinMultiPlayerGameMode",
+ "net/minecraft/client/multiplayer/MultiPlayerGameMode"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinPlayer",
+ "net/minecraft/world/entity/player/Player"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinProjection",
+ "net/minecraft/client/renderer/Projection"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinMobEffectFogEnvironment",
+ "net/minecraft/client/renderer/fog/environment/MobEffectFogEnvironment"
+ );
+
+ // === 输入/物品/方块等 Mixin(7 个)===
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinKeyboardHandler",
+ "net/minecraft/client/KeyboardHandler"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinMain",
+ "net/minecraft/client/main/Main"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinTitleScreen",
+ "net/minecraft/client/gui/screens/TitleScreen"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinItem",
+ "net/minecraft/world/item/Item"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinFlowingFluid",
+ "net/minecraft/world/level/material/FlowingFluid"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinWebBlock",
+ "net/minecraft/world/level/block/WebBlock"
+ );
+ COMMON_MIXINS.put(
+ "com/github/epsilon/mixins/MixinDataComponentInitializers",
+ "net/minecraft/core/component/DataComponentInitializers"
+ );
+ }
+
+ /**
+ * 仅含真正注入逻辑的 Mixin(排除 Accessor 接口,共 38 条)。
+ * 热重定义时通常只需要这个。
+ */
+ public static final Map INJECTION_MIXINS = new LinkedHashMap<>();
+
+ static {
+ for (var entry : COMMON_MIXINS.entrySet()) {
+ String simpleName = entry.getKey().substring(entry.getKey().lastIndexOf('/') + 1);
+ if (!simpleName.startsWith("I")) {
+ INJECTION_MIXINS.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ // ==================== 便捷内部名获取 ====================
+
+ /** 获取 Mixin 类的 internal name */
+ public static String mixinInternalName(String simpleName) {
+ return "com/github/epsilon/mixins/" + simpleName;
+ }
+
+ /** 获取 Target MC 类的 internal name(未包含在表中的返回 null) */
+ public static String targetInternalName(String mixinSimpleName) {
+ return COMMON_MIXINS.get(mixinInternalName(mixinSimpleName));
+ }
+
+}
diff --git a/common/src/main/java/com/github/epsilon/utils/MixinRuntimeLauncher.java b/common/src/main/java/com/github/epsilon/utils/MixinRuntimeLauncher.java
new file mode 100644
index 00000000..24a54124
--- /dev/null
+++ b/common/src/main/java/com/github/epsilon/utils/MixinRuntimeLauncher.java
@@ -0,0 +1,338 @@
+package com.github.epsilon.utils;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.mixin.MixinEnvironment;
+import org.spongepowered.asm.mixin.transformer.IMixinTransformer;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 运行时启动 SpongePowered Mixin 变换管线的工具。
+ *
+ * 解决的问题
+ * 正常 Mixin 通过 classpath 加载目标类和 Mixin 类。本工具让你可以:
+ *
+ * - 用自定义 {@code byte[]} 提供目标类的 raw bytes
+ * - 用自定义 {@code byte[]} 提供Mixin 模板类的 raw bytes
+ * - 触发 Mixin 完整管线:描述符重映射 + {@code @Inject} + {@code @Overwrite} + 字段合并等
+ *
+ *
+ * Mixin 变换管线概述(源码追踪)
+ *
+ * IMixinTransformer.transformClass(env, name, byte[])
+ * → MixinTransformer.transformClass()
+ * → MixinProcessor.applyMixins(env, name, classNode)
+ * → 遍历 MixinConfig → hasMixinsFor(name) → getMixinsFor(name)
+ * → TargetClassContext(env, extensions, sessionId, name, classNode, mixins)
+ * → TargetClassContext.applyMixins()
+ * → MixinApplicatorStandard.apply(mixins)
+ * → 对每个 MixinInfo: mixin.createContextFor(target)
+ * → MixinPreProcessorStandard.createContextFor()
+ * → MixinTargetContext(mixin, classNode, target)
+ * → transformMethod() [描述符重映射]
+ * → prepareInjections() → applyInjections() [@Inject]
+ * → applyFields() / applyMethods() [合并到目标类]
+ *
+ *
+ * 实现原理
+ *
+ * Mixin 管线的核心瓶颈在于:Mixin 类必须通过 MixinConfig 注册,且必须能从 classpath 加载。
+ * 本工具通过反射直接操作 Mixin 内部数据结构({@code MixinConfig.mixinMapping}、
+ * {@code MixinInfo.state}),将自定义 ClassNode 注入到管线中,绕过 classpath 加载限制。
+ *
+ *
+ * 提供两个入口点
+ *
+ * | 入口 | 正常 Mixin | 本工具 |
+ *
+ * | 目标类 bytes |
+ * {@code transformClassBytes(name, _, basicClass)} — classLoader 提供 |
+ * {@code transformClass(env, name, classNode)} — 直接接受 ClassNode |
+ *
+ *
+ * | Mixin 类 bytes |
+ * {@code MixinInfo.loadMixinClass()} → {@code getBytecodeProvider().getClassNode()} — classpath 加载 |
+ * 反射替换 {@code MixinInfo.state} 中的 ClassNode → 自定义 bytes 解析的 ClassNode |
+ *
+ *
+ *
+ * 前置条件
+ *
+ * - Mixin 框架必须已初始化(在 Fabric/NeoForge 环境中默认满足)
+ * - 项目中至少要有一个在 {@code epsilon.mixins.json} 中注册的正常 Mixin 类(本工具复用其 MixinConfig)
+ * - 提供的 Mixin 类 raw bytes 必须包含合法的 {@code @Mixin} 注解
+ *
+ *
+ * @see BytecodeInjector 更轻量的字节码注入(不需要 @Mixin 注解,直接拼方法头)
+ * @see SpongePowered Mixin 源码
+ */
+public final class MixinRuntimeLauncher {
+
+ private MixinRuntimeLauncher() {
+ }
+
+ /**
+ * 一步完成:用自定义 bytes 启动 Mixin 变换管线。
+ *
+ * @param targetClassName 目标类全限定名,如 {@code "net.minecraft.client.Minecraft"}
+ * @param targetRawBytes 目标类的 raw byte[]
+ * @param mixinRawBytes Mixin 模板类的 raw byte[](必须有 {@code @Mixin} 注解)
+ * @return 变换后的目标类 byte[],如果目标不是任何 Mixin 的目标则返回原始 bytes
+ */
+ public static byte[] applyMixin(String targetClassName, byte[] targetRawBytes, byte[] mixinRawBytes) {
+ try {
+ // 1. 获取 Mixin 基础设施
+ MixinEnvironment env = MixinEnvironment.getCurrentEnvironment();
+ Object activeTransformer = env.getActiveTransformer();
+ if (!(activeTransformer instanceof IMixinTransformer transformer)) {
+ throw new IllegalStateException(
+ "No active IMixinTransformer. Mixin may not be initialized yet."
+ );
+ }
+
+ // 2. 将两个 raw bytes 都解析为 ClassNode
+ ClassNode mixinNode = toClassNode(mixinRawBytes);
+ ClassNode targetNode = toClassNode(targetRawBytes);
+ String targetInternalName = targetClassName.replace('.', '/');
+
+ // 3. 核心:把自定义 Mixin ClassNode 注入到 Mixin 内部管线
+ registerMixinWithCustomClassNode(transformer, targetClassName, mixinNode);
+
+ // 4. 触发 Mixin 变换(修改 targetNode 本身,返回 true 表示有变换发生)
+ boolean applied = transformer.transformClass(env, targetInternalName, targetNode);
+
+ if (applied) {
+ return toByteArray(targetNode);
+ }
+ // 未匹配到任何 Mixin,返回原始 bytes
+ return targetRawBytes;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "MixinRuntimeLauncher: Failed to apply mixin to " + targetClassName, e
+ );
+ }
+ }
+
+ /**
+ * 注册一个自定义 Mixin 类到 Mixin 管线中。
+ *
+ * 核心步骤(通过反射操作 Mixin 内部状态):
+ *
+ * - 获取 {@code MixinTransformer → MixinProcessor → configs} 列表
+ * - 取第一个可用 MixinConfig 作为"宿主"
+ * - 取该 Config 中任意一个已有 MixinInfo 作为"模板"(继承其 parent/config/service 引用)
+ * - 创建新的 MixinInfo.State(包装我们的自定义 ClassNode)
+ * - 将 (targetClassName → 新 MixinInfo) 注入到 Config 的 mapping 中
+ *
+ *
+ */
+ private static void registerMixinWithCustomClassNode(
+ IMixinTransformer transformer,
+ String targetClassName,
+ ClassNode mixinClassNode
+ ) throws Exception {
+
+ // ---------- 第 1 步:通过反射获取 MixinProcessor ----------
+ // MixinTransformer 是 package-private final class,IMixinTransformer 是其公共接口
+ Field processorField = findField(transformer.getClass(), "processor");
+ Object mixinProcessor = processorField.get(transformer);
+
+ // ---------- 第 2 步:获取 configs 列表 ----------
+ @SuppressWarnings("unchecked")
+ List