feat: 新增饱和度显示、自定义迷雾、AutoText 快捷消息与 Windows SMTC 集成 - #173
Conversation
新增 SaturationDisplay InterfaceModule + SaturationDisplayComponent: - 默认定位在原版饥饿栏处,可拖动/缩放/换背景 - 绘制原版风格食物图标并按实际饥饿值填充,附带饱和度数值 - 开启模块时通过 MixinGuiIngameForge 取消原版 renderFood,关闭即恢复原版
- CustomFog Render 模块:线性/指数两种模式,颜色、起始距离、结束距离可调 - 在 MixinEntityRenderer.setupFog RETURN 处覆盖雾色与 GL 雾参数 - 默认不影响水下/岩浆迷雾,开启后仅在世界迷雾生效
- AutoText Utility 模块:默认 G→gg,最多 20 条,禁止重复快捷键 - AutoTextSetting(ArrayList<AutoTextEntry>)+ AutoTextEntry 不可变对象 - ClickGUI 编辑器:每行 快捷键绑定 + 消息输入框 + 删除×,底部居中 + 新增 - ConfigManager 增加 autoText 类型的序列化/反序列化,去重并丢弃畸形条目 - Setting.copyValue 对 AutoTextEntry 列表做深拷贝,reset 不会泄漏编辑
- Java 侧:SystemMediaTransportControls 门面 + 工厂(非 Windows 自动降级 no-op) - WindowsSystemMediaTransportControls:JNA 加载 fpsmaster-smtc.dll(native/smtc), C++/WinRT 实现 SMTC 会话,发布标题/歌手/封面/进度,暴露 播放/暂停/上一首/下一首 - SmtcMusicBridge:轮询当前曲目状态并发布快照,封面后台下载为 PNG - MusicManager 构造时创建桥接,shutdown 时释放;控制事件回主线程调度 - build.gradle.kts 增加 JNA 依赖;原生 DLL 资源目录与构建脚本占位
同时为饱和度显示/自定义迷雾/AutoText 新增中英文翻译
gaoyu06
left a comment
There was a problem hiding this comment.
过了一遍。主要问题是两个编译错误:CustomFog 用了 1.8.9 没有的 getEye() / GlStateManager.FogMode,SaturationDisplayComponent 往 int 形参传了 float。本地 ./gradlew build 应该是过不去的,PR 里写 compileJava 已通过,是不是在改动前的分支上编的?
另外几个值得改的:AutoText 有两处会意外往公屏发消息(默认键位写错成 Z + 新增条目自动绑到快捷栏 1 键);CustomFog 的颜色设置目前是无效的,而且会盖掉失明药水的雾;SMTC 那边 GetForCurrentView() 在 Win32 JVM 里必失败,功能整个静默不工作。细节写在行内了。
|
|
||
| // Check if in water or lava — only override if the user opted in | ||
| if (!CustomFog.affectWater.getValue()) { | ||
| net.minecraft.block.material.Material eye = mc.thePlayer.getEye().getMaterial(); |
There was a problem hiding this comment.
1.8.9 没有 getEye(),Entity 上只有 getEyeHeight() / getPositionEyes(float) / isInsideOfMaterial(Material)。下面的 GlStateManager.FogMode 也是 1.9 才加的,1.8.9 只有 setFog(int)。312/316/324/328 四行都是 cannot find symbol。
| } | ||
|
|
||
| java.awt.Color fogColor = CustomFog.color.getColor(); | ||
| GlStateManager.color(fogColor.getRed() / 255f, fogColor.getGreen() / 255f, fogColor.getBlue() / 255f); |
There was a problem hiding this comment.
GlStateManager.color() 改的是顶点色(glColor4f),不是雾色。雾色只能用 glFog(GL_FOG_COLOR, buffer) 设,所以 Color 这个设置现在完全没效果,雾还是原版天空/群系色。
而且这行留下的颜色会带到后面的 renderSky、云、实体渲染,依赖当前颜色的几何体会被染上雾色,三参重载还把 alpha 强制成 1.0。
| } | ||
|
|
||
| @Inject(method = "setupFog", at = @At("RETURN")) | ||
| private void overrideFog(int startCoords, float partialTicks, CallbackInfo ci) { |
There was a problem hiding this comment.
这里没挡失明药水。原版 setupFog 对 blindness 会强制成很短的线性雾(约 5 ~ renderDistance*0.25),现在被无条件盖掉了——开着 CustomFog 把距离拉到 200,中了失明还能看得一清二楚,服务器上算优势项而不是外观改动。建议开头加个 isPotionActive(Potion.blindness) 直接 return。
水/岩浆那两个开关打开时也有类似问题:密度换了但原版水下雾色没换,会出现颜色和密度对不上的效果。
| } | ||
|
|
||
| private void drawIcon(float x, float y, int textureX, int textureY) { | ||
| Gui.drawModalRectWithCustomSizedTexture(x, y, textureX, textureY, ICON_SIZE * scale, ICON_SIZE * scale, 256f, 256f); |
There was a problem hiding this comment.
1.8.9 的签名是 (int x, int y, float u, float v, int width, int height, float tw, float th),x/y/width/height 都是 int,这里传的是 float,编不过。
utils/render/draw/Images.java:169 有全 float 的版本,用那个就行——顺便也能保住非整数 HUD scale 下的亚像素对齐,不然图标会抖。
| public class AutoText extends Module { | ||
| private static final ArrayList<AutoTextEntry> DEFAULT = new ArrayList<>(); | ||
| static { | ||
| DEFAULT.add(new AutoTextEntry(0x2C /* G key LWJGL */, "gg")); |
There was a problem hiding this comment.
0x2C 是 KEY_Z,KEY_G 是 0x22。现在的效果是按 G 没反应,按 Z 往公屏发 gg。
| if (addClick != null) { | ||
| // Find first unused key code | ||
| int newKey = 0; | ||
| for (int k = 2; k < 256; k++) { |
There was a problem hiding this comment.
newKey 初始化成 0 之后马上被这个循环覆盖成最小可用扫描码,只有一条已有条目时第一次点 + 就是 2,也就是 KEY_1。
用户输完消息关掉 GUI 不会注意到有绑定,之后战斗里每次切到快捷栏第 1 格都往公屏发一次,很容易被服务器反刷屏禁言。建议新条目保持 keyCode 0,onKey 里跳过未绑定的。
| EventDispatcher.dispatchEvent(new EventRender2D(partialTicks)); | ||
| } | ||
|
|
||
| @Inject(method = "renderFood", at = @At("HEAD"), cancellable = true) |
There was a problem hiding this comment.
Forge 的 renderFood 末尾有 right_height += 10,在 HEAD cancel 会把它一起跳过。右侧后面的 renderHealthMount 和 renderAir 都按 height - right_height 定位,结果氧气泡和坐骑血条整体下移 10px 压到快捷栏上。cancel 之前手动补一下 right_height 就好。
| winrt::init_apartment(winrt::apartment_type::single_threaded); | ||
|
|
||
| // Get the system transport controls | ||
| g_smtc = SystemMediaTransportControls::GetForCurrentView(); |
There was a problem hiding this comment.
GetForCurrentView() 需要 CoreWindow,MC 的 JVM 是普通 Win32 进程,这里必抛,然后被下面的 catch (...) 吃掉,g_smtc 一直是 null。
而 smtc_start 返回 void 不报错,Java 侧照样把 available 设成 true(WindowsSystemMediaTransportControls.java:122),后面 publish / set_buttons 各自在 if (!g_smtc) return 静默返回——功能整个不工作,日志里也看不出任何东西。桌面进程要走 ISystemMediaTransportControlsInterop::GetForWindow(hwnd),估计 smtc_start 收的那个没用上的 hwnd 本来就是给它留的。
| return; | ||
| } | ||
| // Only refresh when we don't already have art for the current track | ||
| if (artwork.get() != null) { |
There was a problem hiding this comment.
只要缓存里有东西就 return,所以封面只会取到第一首歌的。切歌时走到这里 artwork.get() 还是上一首的,下载被跳过,之后整个会话 Windows 那边都显示第一首的封面。清缓存的条件(URL 为空)跟切歌需要的正好反着。
| if (artwork.get() != null) { | ||
| return; | ||
| } | ||
| final Thread t = new Thread(() -> { |
There was a problem hiding this comment.
这里自己起线程跑 ImageIO,绕开了 MusicTextures 的单线程 IMG_EXEC。那边注释写得挺明确:macOS(尤其 Rosetta)下并发调 AWT/CoreGraphics 会崩 objc_release。bridge 是不分平台轮询的,所以 mac 上 HUD 封面和这里同时解码有几率直接崩 JVM。
另外 downloadPng 和 UA 常量基本是照抄 MusicTextures 的,复用一下就行。
编译通过确实是在第一个提交的时候,后面并没有完全验证,为错误的报告致歉 |
- smtc.cpp: 切换到 GetForWindow 桌面互操作路径,smtc_start 返回状态码 - 修复 __stdcall→__cdecl 后导出符号与 JNA 绑定不匹配的问题(DLL 重编译) - 封面图下载改走 MusicTextures 单线程 AWT/ImageIO 队列,避免 macOS 并发崩溃 - 新增 smtc.def 导出定义
- MixinEntityRenderer: 雾色改用 glFog(GL_FOG_COLOR),复位顶点色防污染后续渲染; 失明药水跳过覆盖;水/岩浆材质检查改用 isInsideOfMaterial - MixinGuiIngameForge: renderFood 被 cancel 时手动补 right_height += 10,修复 血量/氧气条整体下移 10px - MixinSplashScreen: @overwrite 补 remap = false - SaturationDisplayComponent: 图标绘制改走 Images 全 float 版本
- 默认快捷键从错误的 0x2C(Z) 修正为 0x22(G) - 新增条目 keyCode 保持 0(None),由用户显式绑定,避免误占键位
|
自定义迷雾的、性能里的performance.reuselevel、切换潜行的fontshadow、、还有performancehud的大量的,还有modslist的round,fontshadow,语言文件缺失了,稍后补齐一下。 |
- performance.reuselevel 及其模式值(保守/均衡/激进) - customfog.affectwater/affectlava - performancehud 共享外观设置(背景/圆角/字体阴影等) - modslist.round/fontshadow、togglesneak.fontshadow、sprint.fontshadow - betterchat.round、betterscreen、blockindicator、hideindicator、 inventorydisplay、taboverlay、targetdisplay 的共享设置 - minimap.fastrender.disable.title(小地图与快速渲染不兼容提示)
线性雾默认 StartDistance 0 / EndDistance 20 让雾色覆盖 0~20 格 近景,近地面整体被染色。默认值推远为 32/64 格,StartDistance 上限从 20 扩到 200 便于拉开区间;mixin 中 end<=start 时自动 修正,避免线性雾公式反转导致近处更浓。
Buffer(capacity) 只设置 Capacity,Length 仍为 0,而 IOutputStream::WriteAsync 只传输 Length 个字节,导致缩略图流永远是 0 字节、媒体控件显示空白封面。 注意:src/main/resources/native/windows/ 下的预编译 DLL 需要重新编译才会包含此修复。 Co-Authored-By: Claude <noreply@anthropic.com>
requestArtwork 之前只更新 artworkUrl 不清 artwork,新歌的标题会配着上一首的封面 发布;若新封面下载失败(404/超时),回调里的 png != null 守卫直接跳过,旧图会 一直留到会话结束。 Co-Authored-By: Claude <noreply@anthropic.com>
setupFog 每帧被调用多次(renderWorldPass 两次 + renderCloudsCheck),每次新建 direct FloatBuffer 会持续走 Bits.reserveMemory 和 Cleaner 注册。原版自身也是缓存 fogColorBuffer 复用的。 Co-Authored-By: Claude <noreply@anthropic.com>
Images.drawModalRectWithCustomSizedTexture 用 width/height 同时决定四边形尺寸和 UV 跨度,HUD scale != 1 时会把 icons.png 里相邻的饥饿/护甲图标一起采样进来。新增 可单独指定 UV 跨度的重载,饱和度图标固定采样 9x9。 Co-Authored-By: Claude <noreply@anthropic.com>
gaoyu06
left a comment
There was a problem hiding this comment.
又过了一遍新的几个提交。先说好消息:上一轮的编译错误确实修好了,我在本地 worktree 上跑了 ./gradlew compileJava,通过,只剩两个跟这个 PR 无关的 mixin warning。失明药水 guard、AutoText 键位和新条目不绑键也都到位了。
有 4 个我直接推到这个分支上了(都单独一个 commit,不合适就 revert 掉):
fix(smtc): WriteAsync 前设置 Buffer.Length— 这个比较要命,见行内fix(smtc): 切歌时清空封面缓存— 上轮第 8 条只修了一半fix(fog): 复用 fog color buffer— 改成 glFog 是对的,但每次都新建 direct bufferfix(saturation): 图标采样跨度与缩放解耦— 编译是修了,但 UV 跨度也跟着 scale 放大了
注意:Buffer.Length 那个改的是 .cpp,src/main/resources/native/windows/ 下那两个预编译 DLL 我没法在 macOS 上重编,得你在 Windows 上重新编一次才会生效,否则封面还是空白的。
剩下几个偏设计取舍的没动,写在行内了:Forge 事件语义、COM 套间、窗口句柄、IMG_EXEC 的线程模型。
另外那两个 DLL 没法做行内评论所以放这儿说:native/smtc/ 现在只有 .cpp 和 .def,没有 CMakeLists / build.bat / gradle task,AGENTS.md 和 docs 里也没写怎么构建。结果就是没人能确认发布的 DLL 是不是从当前 .cpp 编出来的(比如上面那个 Buffer.Length bug 到底在不在已提交的二进制里,我就没法判断),以后改 .cpp 也会静默发布陈旧的二进制。而且这两个文件会打进每个 jar,包括 Linux/macOS 的构建。建议加个构建脚本 + 校验和,或者改成版本化依赖拉取。
| if (artwork_data && artwork_len > 0) { | ||
| try { | ||
| auto stream = InMemoryRandomAccessStream(); | ||
| auto buffer = Buffer(static_cast<uint32_t>(artwork_len)); |
There was a problem hiding this comment.
已在 fix(smtc): WriteAsync 前设置 Buffer.Length 里改了,这里说下原因:
以容量构造的 WinRT Buffer 只有 Capacity == artwork_len,Length 仍然是 0,而 IOutputStream::WriteAsync 只传输 buffer.Length() 个字节。所以从 array_view 换成 Buffer 之后,InMemoryRandomAccessStream 实际上是 0 字节,CreateFromStream 包了个空流,媒体浮出控件永远显示空白占位图——Java 侧刚修好的整条封面管线到这一步全丢了。
补一行 buffer.Length(static_cast<uint32_t>(artwork_len)); 就行。但 DLL 得重编,否则这个修复不生效。
| if (g_initialized) return 1; | ||
| try { | ||
| // Initialize WinRT apartment (MTA so callbacks can fire on any thread) | ||
| winrt::init_apartment(winrt::apartment_type::multi_threaded); |
There was a problem hiding this comment.
这个没动,属于设计取舍。
MusicManager 是从 MusicScreen(主渲染线程)懒构造的,所以 smtc_start 跑在游戏线程上。如果那个线程上已经有人以 STA 调过 CoInitializeEx/OleInitialize(LWJGL 剪贴板/拖放、其他用 Desktop.browse 或 shell API 的 Forge 模组),CoInitializeEx(COINIT_MULTITHREADED) 会返回 RPC_E_CHANGED_MODE,init_apartment 抛异常被 catch (...) 吞掉返回 0。用户看不到媒体控件,日志只有一句 "failure status",没有 HRESULT。
反过来成功的时候,游戏线程被永久加入 MTA(smtc_close 从不调 uninit_apartment),后续需要 STA 的 shell 调用可能受影响。
比较稳的做法是把 SMTC 放到自己的专用线程上初始化并跑消息循环,别借用游戏线程。
|
|
||
| // Find a visible top-level window owned by this process. GetForWindow needs a | ||
| // real HWND; the Java side passes 0 when it has no window handle to hand over. | ||
| static HWND find_process_window() { |
There was a problem hiding this comment.
Java 侧永远传 0,所以 GetForWindow 拿到的是 EnumWindows 对这个 PID 恰好先返回的那个窗口。Forge 启动期间那可能是 SplashProgress 窗口或者某个 AWT/Swing 框架窗口——SMTC 会把会话绑到一个很快就被销毁的窗口上,控件从浮出面板静默消失,而 smtc_start 返回 1、Java 报 available == true,排查不出来。
另外 static HWND found 是进程级共享的(无状态 lambda 没法捕获),两个并发 smtc_start 会互相覆盖。
专门为此加的 HWND 参数其实是对的,缺的是让 Java 侧真的把 LWJGL 的窗口句柄传进来。
| } | ||
|
|
||
| /// Get the last error message (for debugging). | ||
| __declspec(dllexport) void __cdecl smtc_get_last_error(wchar_t* buf, int bufLen) { |
There was a problem hiding this comment.
smtc.def 把 smtc_get_last_error 导出了,但函数体只有 (void)buf; (void)bufLen;,而 SmtcNative 里根本没声明它。
所以同一个提交里新加的 status != 1 告警只能告诉用户 "failure status"——正好是这个导出本来要提供的信息拿不到。要么实现它并写进失败日志,要么连 .def 条目一起删掉。
| * 供 SMTC 等需要原始字节(而非 GL 纹理)的调用方复用同一套下载/解码路径, | ||
| * 避免自起线程并发调用 AWT/ImageIO 在 macOS 上触发崩溃。 | ||
| */ | ||
| public static void downloadPngAsync(final String url, final java.util.function.Consumer<byte[]> callback) { |
There was a problem hiding this comment.
改走 downloadPngAsync 方向是对的,但下载和调用方回调都跑在同一个 IMG_EXEC 上,这个单线程是 cover()、base64Image()、qr() 共用的。
切歌会额外排一次同一 URL 的重复下载(MusicTextures.cover() 已经为 UI 取过一次),连接 10s + 读 15s;完成回调又在同一线程里调 controls.publish() → JNA → smtc_publish,里面是同步的 stream.WriteAsync(buffer).get()。这期间队列上其他图片都解不了码——QQ/网易云扫码登录的二维码最多空白 25 秒,用户会以为登录坏了。
要么给下载单开一个执行器、只把解码放回 IMG_EXEC,要么让回调里的 publish 换个线程。
| if (SaturationDisplay.using) { | ||
| // renderFood 末尾的 right_height += 10 会在 cancel 时被跳过, | ||
| // 手动补上以免 renderHealthMount/renderAir 整体下移 10px。 | ||
| right_height += 10; |
There was a problem hiding this comment.
补 right_height 是对的,但有两个边角:
- Forge 的
renderFood开头是if (pre(FOOD)) return;,right_height += 10在后面。也就是说别的 HUD 模组(小地图/HUD 管理器)在Pre(FOOD)里取消食物元素时,原本right_height是不变的,而这里无条件加了 10,氧气条和坐骑血条会比那个模组预期高 10px。 - 因为是在 HEAD 取消,
ForgeHooks的 Pre/Post FOOD 事件根本不会触发,在Post(FOOD)里画自己食物条的模组会静默停渲染。
改成注入在 pre(FOOD) 之后(或者用 Pre(FOOD) 事件监听器 cancel)能同时解决这两点。不过如果只面向自家用户、不考虑和别的 HUD 模组共存,现在这样也够用。
native/smtc/CMakeLists.txt 用 MSVC 静态 CRT 编 x64/Win32 两份 DLL;CI 新增 build-native 作业(windows-latest)构建后作为 artifact 传给打包作业,注入 src/main/resources/native/windows/ 再执行 gradle build,并校验两个 DLL 非空。 已提交的二进制随之删除并加入 .gitignore:此前无法确认发布的 DLL 是否与当前 smtc.cpp 一致,改 .cpp 也会静默发布陈旧二进制。 Co-Authored-By: Claude <noreply@anthropic.com>
套间状态是线程私有的:在游戏线程上 init_apartment 要么撞 RPC_E_CHANGED_MODE (别的模组已把该线程初始化成 STA)静默失败,要么把游戏线程永久并入 MTA。改为 所有 native 调用都走单线程 FPSMaster-SMTC,顺带把阻塞的 smtc_publish 从调用方 线程上摘下来(发布用最新快照覆盖,不排队)。 同时: - smtc_start 的 hwnd 改传 LWJGL 窗口句柄(反射 WindowsDisplay.hwnd),JNA 侧 由 long 改为 Pointer,x86 下 HWND 是 32 位,long 会传错宽度 - native 兜底的窗口搜索加上可见/无 owner/有标题过滤,并去掉非重入的函数内 static,避免绑到马上就被销毁的 Forge 启动画面窗口 - 实现 smtc_get_last_error 并在启动失败时打进日志,此前它是空实现且没绑进 JNA - init_apartment 成功时才在 smtc_close 里 uninit_apartment Co-Authored-By: Claude <noreply@anthropic.com>
renderFood 第一句就是 if (pre(FOOD)) return;,在 HEAD 取消会让 Pre(FOOD) 事件 根本不派发,别的 HUD 模组的钩子静默失效。改为重定向这次 pre 调用:事件照常触发, 别人已取消时把占位交给他们,只有我们真正接管时才补 right_height += 10。 Co-Authored-By: Claude <noreply@anthropic.com>
C++/WinRT 头文件会拉协程支持,/std:c++17 下解析到 <experimental/coroutine>, 当前 MSVC 直接报 STL1011 错误(CI 实测 x64/Win32 均失败)。C++20 走标准 <coroutine>。 Co-Authored-By: Claude <noreply@anthropic.com>
原实现取消原版 renderFood,然后用同一套 icons.png 贴图把饥饿条原样重画一遍, 右边挂一个饱和度数值。真正新增的信息只有那个数值,但为了重画饥饿条付出的代价是: right_height 补偿、Pre/Post(FOOD) 事件失效、图标 UV 采样、与第三方 HUD 模组冲突。 改为只画饱和度本身(一根 0-20 的条加数值),原版饥饿条照常渲染。随之删除: - MixinGuiIngameForge 对 renderFood 的接管(连同 right_height 影子字段) - Images 里为缩放采样加的 UV 重载(恢复成原来的 private 方法) 新增 BarColor 设置,默认位置移到饥饿条上方。 实机验证:原版饥饿条恢复正常,饱和度条在 HUD scale=2 下不再糊。 Co-Authored-By: Claude <noreply@anthropic.com>
雾在 1.8.9 里是两套状态:setupFog 设 GL_FOG_COLOR 只影响几何体,而天空和清屏色 走的是 EntityRenderer.fogColorRed/Green/Blue(renderWorldPass 拿它 glClearColor, renderSky 拿它画地平线雾带)。此前只改了前者,结果是方块被自定义雾吃掉、天空还是 原版蓝,实机看上去割裂。现在注入 updateFogColor 一并覆盖。 顺带: - 覆盖条件抽成 fpsmaster$shouldOverrideFog(),两处注入共用同一套判断 - 雾色 buffer 改用原版 setFogColorBuffer(它写的是 EntityRenderer 自己缓存的 direct buffer),删掉上一版自己加的静态缓存 - 删掉 GlStateManager.color(1,1,1,1) 复位:它是为配套已删除的顶点色写入而加的, 现在这里不再动顶点色,留着反而会覆盖调用方的颜色状态 已知残留:地平线仍有一条原版天空色,那来自 WorldClient.getSkyColor,是独立于 fogColor 的第三处颜色,本次未动。 Co-Authored-By: Claude <noreply@anthropic.com>
|
在 macOS 上把客户端真跑起来验证了一遍,补充几条实机结论。 已验证生效的:
一个还没定位的问题,留给后面处理: 开着 CustomFog 时,地平线会横着留一条原版天色的带子,把它关掉画面立刻恢复正常,所以是 CustomFog 自己造成的,不是既有问题。 我排掉的可能性:
所以它来自 顺带提醒:改 |
IMG_EXEC 是单线程,约束来自 AWT——macOS(尤其 Rosetta) 下并发调 CoreGraphics 会崩。 但 HTTP 下载没有这个约束,却跟着一起排队:一次封面下载超时(连接 10s + 读 15s)会把 后面所有解码堵死,扫码登录的二维码明明不需要联网也要跟着等最多 25 秒。 拆成两个池:NET_EXEC 跑网络,拿到字节再回 IMG_EXEC 解码。串行解码这个约束没有变化, qr()/base64Image() 仍然全程在 IMG_EXEC 上。 顺带把 downloadPngAsync 改成 downloadBytesAsync:SMTC 的 RandomAccessStreamReference 吃的就是图片流,Windows 自己会解码、JPEG 也认,原先「下载 → ImageIO.read → ImageIO.write 编回 PNG」那一读一写是白做的,还把这条路径绑在了串行队列上。现在 SMTC 取封面完全不碰 AWT。相应地 MediaPlaybackSnapshot.artworkPng 改名 artworkBytes。 Co-Authored-By: Claude <noreply@anthropic.com>
本次 PR 一次性引入四个新功能,并为现有 BlockOverlay 修复了一个 NPE:
详细变更
饱和度显示(SaturationDisplay)
编辑器拖动/缩放/换背景
自定义迷雾(CustomFog,Render 分类)
AutoText(AutoText,实用分类)
类型),加载时去重、丢弃畸形条目
Windows SMTC(音乐功能接入系统媒体控件)
DLL(native/smtc/smtc.cpp,C++/WinRT),实现完整 SMTC 会话
修复
验证情况
src/main/resources/native/windows/{x64,x86}/,未放置时自动降级为 no-op