Skip to content

SDGB 1.56 调试实录:从 AquaMai 崩溃到独立实现下屏位置与缩放 ​

本文记录一次围绕 SDGB 1.56、Sinmai-Assist、AquaMai 与 MelonLoader 的完整调试过程。目标不是修改整个游戏窗口,而是在不加载完整 AquaMai 的前提下,只调整游戏内部下侧圆形屏幕的位置和大小,使其与实体控制器的中心准确对齐。

一、问题背景 ​

我的 SDGB 界面由上下两部分组成:上侧是矩形信息屏,下侧是圆形游戏主屏。在实体控制器上使用时,下侧圆形画面的中心、直径和实际控制器存在偏差。我希望能够在游戏运行过程中调整下屏的位置和缩放比例,同时不影响上屏。

AquaMai 中原本提供了“屏幕位置调整”功能,但当前完整的 AquaMai.dll 与我的 SDGB 1.56 环境存在兼容性问题。只要加载完整 AquaMai,游戏就可能在启动或进入选歌界面后崩溃。因此,整个调试过程最终转向了一个更明确的目标:

提取 AquaMai 屏幕位置调整功能所需的最小逻辑,制作一个独立的 MelonLoader Mod,而不是继续修补完整 AquaMai。


二、第一次故障:AquaMai 在选歌界面崩溃 ​

最初的主要异常是:

text
System.MissingMethodException:
No matching PacketUpsertUserAll constructor found

调用栈指向 AquaMai 的:

text
AquaMai.Core.Helpers.Shim

日志还出现了类似以下提示:

text
Failed to patch AquaMai.Core.Helpers.Shim
No matching PacketUpsertUserAll constructor found

这说明问题不在触摸串口、外部控制器或启动脚本,而在 AquaMai 对当前游戏程序集的兼容性上。

AquaMai 会通过反射或补丁逻辑查找 PacketUpsertUserAll 的特定构造函数,但当前 SDGB 1.56 的 Assembly-CSharp.dll 中,相关类或构造函数结构与 AquaMai 预期不一致。最终形成了下面的故障链:

text
AquaMai 初始化 Shim
→ 找不到预期构造函数
→ 后续模块仍调用 Shim
→ 进入选歌或网络处理阶段时崩溃

关闭 AquaMai 虽然可以避开异常,但也会同时失去由 AquaMai 提供的部分输入功能,因此不能把“关闭 AquaMai”当作最终解决方案。


三、覆盖更新包带来的版本混用风险 ​

在升级 SDGB 时,我曾使用两个压缩包覆盖旧目录:

  • 1.56覆盖包.zip
  • amfs覆盖.zip

检查后发现,1.56覆盖包.zip 只包含部分文件,例如:

text
amdaemon.exe
AMDaemon.NET.dll
Assembly-CSharp-firstpass.dll
ChimeLib.NET.dll
amdaemon_api.dll
chimelib_dll.dll
resources.assets

但其中没有:

text
Assembly-CSharp.dll
Sinmai.exe
UnityPlayer.dll
AquaMai.dll
MelonLoader

这意味着它更接近“局部更新包”,而不是一个完整、可独立使用的 1.56 程序包。如果在旧目录上直接覆盖,就可能形成:

text
部分 1.56 文件
+ 旧版或其他来源的 Assembly-CSharp.dll
+ 不匹配的 AquaMai.dll

这种混合环境很容易产生反射失败、方法签名不一致和补丁目标找不到等问题。

哈希核对 ​

我对覆盖包中的关键文件进行了 MD5 核对:

text
amdaemon.exe
915a494f9aa45f85d928f00a6cdf46cb

AMDaemon.NET.dll
8b55e6800fc7b871b402fee2cbfb0642

Assembly-CSharp-firstpass.dll
c36d036aff69fa839a436f2fc90b19e8

ChimeLib.NET.dll
40e48691919fcb841509d22f3c93d04a

amdaemon_api.dll
285f12a50d4e5e682b7858cbf34f2611

chimelib_dll.dll
c4f916c4f46dc17a2ae428a0782215d3

当前目录中的这些文件与覆盖包一致,但 Assembly-CSharp.dll 并不在覆盖包中,无法由该覆盖包完成版本闭环。

经验 ​

更新这类环境时,不能只确认“文件存在”,还要确认:

  1. 文件来源一致;
  2. 文件版本属于同一套程序;
  3. 关键 DLL 的哈希能够对应;
  4. 不在旧目录上长期叠加不明来源的局部覆盖包。

四、重新构建后,Sinmai-Assist 缺少依赖 ​

为了避开完整 AquaMai,我重新构建了一套使用 Sinmai-Assist 的 SDGB 环境。

MelonLoader 能识别 Sinmai-Assist.dll,但日志显示:

text
'Sinmai-Assist' is missing the following dependencies:
- 'netstandard' v2.0.0.0
- 'Swan.Lite' v3.1.0.0

随后还出现:

text
ReflectionTypeLoadException
Could not load file or assembly 'netstandard'
BadImageFormatException

这说明日志中的:

text
1 Mod loaded

并不等于 Sinmai-Assist 已经完整工作。Mod 虽然被识别,但类型加载、Harmony Patch 和 GUI 绘制都处于异常状态。

处理方法 ​

优先检查:

powershell
Get-ChildItem -Recurse -Filter netstandard.dll
Get-ChildItem -Recurse -Filter Swan.Lite.dll

依赖文件应放在 MelonLoader 能提前加载的位置,例如:

text
Package\UserLibs\netstandard.dll
Package\UserLibs\Swan.Lite.dll

只有以下错误消失后,才适合继续排查输入和游戏功能:

text
missing dependencies
ReflectionTypeLoadException
Could not load file or assembly 'netstandard'
BadImageFormatException

日志还出现过:

text
DisableEnvironmentCheck, Method Not Found!

这说明 Sinmai-Assist 中的部分补丁也可能没有完全适配当前 SDGB 版本。不过它的优先级低于依赖缺失,应先确保基础加载正常,再处理方法匹配问题。


五、二维码摄像头不工作:DummyLogin 与真实摄像头互斥 ​

在配置二维码读取摄像头时,日志显示:

text
DummyLogin enabled, CustomCameraId has been automatically disabled.

当时配置中同时启用了:

yml
dummyLogin:
  enable: true

customCameraId:
  enable: true

这两个功能在当前 Sinmai-Assist 逻辑中存在冲突。启用虚拟登录后,真实摄像头配置会被自动禁用。

需要真实二维码摄像头时,应先关闭 DummyLogin:

yml
common:
  dummyLogin:
    enable: false

  customCameraId:
    enable: true
    chimeCameraId: 0
    leftQrCameraId: 0
    rightQrCameraId: 0
    photoCameraId: 0

  changeGameSettings:
    enable: true
    codeRead: true

如果日志仍提示找不到摄像头,则依次测试:

text
Camera ID: 0 → 1 → 2 → 3

这一步带来的经验是:排查配置问题时,不能只看目标功能是否启用,还必须检查是否存在会自动关闭它的上游功能。


六、同一个 BAT 在不同机器上表现不同 ​

后来出现了另一个问题:同一个启动 BAT 在两台电脑上运行,其中一台的 inject 窗口没有正常出现。

最初很难判断是:

  • BAT 没有执行到 inject;
  • inject 启动后立即退出;
  • 杀毒软件拦截;
  • 运行库缺失;
  • 路径或工作目录不正确;
  • 目标文件缺失。

为此,我制作了一个调试版启动脚本,主要增加了:

  • 可见的 inject 控制台;
  • 启动步骤日志;
  • 必要文件检查;
  • 必要目录检查;
  • 进程状态检查;
  • 退出码记录;
  • 固定工作目录。

例如:

bat
cd /d "%~dp0"

start "inject-debug" cmd /k ^
".\inject.exe -d -k .\mai2hook.dll .\amdaemon.exe -f -c .\config_common.json .\config_server.json .\config_client.json"

最终原因:缺少 option 目录 ​

调试窗口成功暴露了真正原因:其中一套 SDGB 缺少必要的 option 目录,导致 inject 初始化失败。补充目录后,程序恢复正常。

为避免空目录在复制、压缩或同步时再次丢失,我把目录检查写入启动脚本:

bat
if not exist "option" (
    echo [WARN] option directory not found, creating...
    mkdir "option"
)

if not exist "option" (
    echo [ERROR] Failed to create option directory.
    pause
    exit /b 1
)

也可以在目录中保留一个占位文件:

text
option\.keep

运行环境的完整性不仅包括文件,也包括必要的空目录。调试脚本最大的价值不是“自动修复一切”,而是让原本一闪而过的异常变得可见、可记录。


七、第一次屏幕调整尝试:从外部移动整个窗口 ​

为了绕开 AquaMai,我首先尝试使用 PowerShell 调整游戏窗口:

text
F10          进入或退出调整模式
方向键       移动窗口
+ / -        缩放窗口
F9           输出当前位置
Esc          退出脚本

外部脚本通过 Win32 API 调用:

text
MoveWindow
GetWindowRect
EnumWindows

并尝试按以下条件识别游戏窗口:

text
ProcessName = sinmai
WindowTitle contains Sinmai
WindowClass = UnityWndClass
Exclude ConsoleWindowClass

为什么失败 ​

实际运行时,MelonLoader 控制台窗口会与 Sinmai 游戏进程同时出现。仅凭进程名、窗口标题或窗口类,很难稳定区分:

  • Unity 游戏主窗口;
  • MelonLoader 控制台;
  • amdaemon 控制台;
  • 其他辅助窗口。

即使偶尔选中了正确窗口,这个方案调整的仍然是整个 Windows 窗口,而不是游戏内部的下屏区域。

这里必须区分两个概念:

text
整个游戏窗口的位置和分辨率
≠
游戏内部上下屏区域的位置和缩放

我的真实目标是只调整下侧圆形区域,因此外部窗口脚本从一开始就不是最合适的技术路线。


八、重新理解 AquaMai 的“屏幕位置调整” ​

AquaMai 中存在两类容易混淆的功能。

1. 整体窗口与分辨率设置 ​

它负责:

text
窗口化
无边框
整体分辨率
Windows 窗口样式

这一类通常基于:

csharp
Screen.SetResolution(...)
SetWindowLongPtr(...)
SetWindowPos(...)

2. 游戏内部屏幕位置调整 ​

我真正需要的是 ScreenPositionAdjust。它不是简单移动某个 UI 对象,也不是调整整个 Windows 窗口,而是重新组织游戏的渲染输出。

AquaMai 的核心思路是:

text
原游戏画面
→ Camera 捕获
→ RenderTexture
→ 新建显示 Canvas
→ 将上下屏拆分成 RawImage
→ 分别调整位置和缩放

因此,正确方向不是继续完善 PowerShell 脚本,而是制作一个最小 MelonLoader Mod,只复刻这部分内部显示逻辑。


九、SinmaiScreenAdjustLite 的开发过程 ​

最终我将独立 Mod 命名为:

text
SinmaiScreenAdjustLite

它不引用完整 AquaMai,也不包含:

text
Shim
PacketUpsertUserAll
NetPacketHook
FixLevelDisplay
解锁功能
网络包处理
输入映射

只保留下屏显示调整所需的逻辑。


十、v0.1.0:直接扫描并缩放 Unity 对象 ​

第一版采用最直观的方法:

  1. 扫描场景中的 GameObject、Transform、RectTransform 和 Camera;
  2. 输出疑似下屏对象;
  3. 自动选择类似路径:
text
LeftMonitor/.../Canvas/Main
  1. 修改目标对象的:
text
localPosition
localScale

默认配置为:

ini
[LowerScreen]
Enable=true
OffsetX=0
OffsetY=0
Scale=1.0
TargetPath=

为了适配不同流程中不断变化的对象名称,后续加入了通配路径:

ini
TargetPath=LeftMonitor/*/Canvas/Main

这样可以匹配:

text
PowerOnProcess(Clone)
GenericProcess(Clone)

对象能够被找到,但它并不等于完整的圆形下屏渲染结果。


十一、v0.2.0:加入 F10 动态调整 ​

第一版只支持固定配置,因此按 F10 没有反应。v0.2.0 才真正加入交互功能:

text
F10          开启/关闭调整模式
方向键       移动下屏
+ / -        调整缩放
F9           输出当前参数
Enter        保存配置

调整模式中会显示:

text
OffsetX
OffsetY
Scale

这一步解决了交互问题,但没有解决“缩放对象本身是否正确”的问题。


十二、v0.3.0:从 Canvas/Main 改到 Background ​

实际测试发现:

text
LeftMonitor/.../Canvas/Main

是一个较大的容器,其中还包含:

text
MainMessage
SubMessage

因此缩放 Canvas/Main 时,游戏中的信息提示栏也会一起变化。

v0.3.0 将目标收窄为:

ini
TargetPath=LeftMonitor/*/Canvas/Main/Background

同时,F9 增加了目标父级和子级对象的诊断输出。

但 Background 仍然只是流程中的一个背景 UI,而不是完整的下屏最终画面。直接修改单个 Unity UI 对象的路线到这里基本确认不可行。


十三、v0.4.0:改用 Camera 与 RenderTexture 重渲染 ​

v0.4.0 开始采用更接近 AquaMai 的实现:

text
Capture Camera 捕获原始画面
→ 输出到 RenderTexture
→ 新建显示层
→ 上屏保持原位
→ 下屏作为独立 RawImage
→ F10 只调整下屏 RawImage

这一步解决了“提示栏被当成缩放对象”的问题,因为调整对象不再是原始场景中的 Canvas/Main 或 Background。

日志中可以看到类似:

text
SinmaiScreenAdjustLite v0.4.0
RenderTexture mode initialized
Applied RenderTexture LowerScreen adjustment

新问题 ​

切换到重渲染后,一些原本不应该显示的 Unity 元素轮廓也被带入了画面。


十四、v0.4.1:显示层与捕获层隔离 ​

为了避免 Capture Camera 拍到 Lite 自己的新显示层,v0.4.1 增加了专用 Unity Layer:

text
Lite Display Canvas → 专用 layer 31
Display Camera → 只渲染 layer 31
Capture Camera → 排除 layer 31

同时:

  • Capture Camera 使用黑色背景清屏;
  • F9 输出 Camera 的 layer 与 cullingMask;
  • 避免显示层形成回授画面。

日志示例:

text
DisplayLayer=31
CaptureCullingMask=0x7FFFFEFF
DisplayCullingMask=0x80000000

这一版减少了部分额外元素,但仍没有完全消除轮廓。


十五、建立可验证的诊断方法 ​

为了避免继续盲目调整,我在后续版本中加入了几个诊断开关:

ini
SolidLowerColorTest=false
DisableOriginalCamerasAfterSetup=false
DumpRenderPipeline=true
CircularLowerMask=false

SolidLowerColorTest ​

用纯色替换下屏纹理。它可以判断:

text
纯色模式仍有轮廓
→ 轮廓来自 RenderTexture 背后的原始输出

纯色模式轮廓消失
→ 轮廓来自 Capture Camera、UV 采样或 RenderTexture 内容

DisableOriginalCamerasAfterSetup ​

在 Lite 的 Capture Camera 与 Display Camera 初始化完成后,临时禁用其他 Camera,用于判断是否仍有原始相机直接输出到窗口。

DumpRenderPipeline ​

输出所有 Camera 与 Canvas 的:

text
名称
enabled
depth
clearFlags
backgroundColor
cullingMask
targetTexture
rect / pixelRect
Canvas renderMode
sortingOrder
worldCamera
targetDisplay

CircularLowerMask ​

给下屏增加圆形遮罩,用于判断边缘轮廓是否来自矩形 RawImage 暴露区域。

这些开关把“画面不对”转化成了能够逐项验证的问题。


十六、v0.5.0:最终定位到错误的 uvRect ​

最终检查发现,旧版真正的核心问题是:

RenderTexture 的下屏采样区域并不是正方形。

在 1920×1080 环境中,旧版实际采样了:

text
1920 × 607.5

这不是一个适合圆形下屏的正方形区域。即使后续使用圆形遮罩,也会把不属于下屏的画面内容一起采样进来,导致边缘轮廓、额外 UI 和比例异常。

参考 AquaMai 修正后的参数 ​

v0.5.0 将参数修正为:

text
RenderTexture: 1215 × 1080
1P 下屏 uvRect: (0, 0, 0.5, 0.5625)

实际采样尺寸为:

text
宽度  = 1215 × 0.5    = 607.5
高度  = 1080 × 0.5625 = 607.5

因此得到:

text
607.5 × 607.5

日志确认:

text
sampledRegionIsSquare=True

这一修正确保了下屏采样区域本身就是正方形,圆形画面才可能完整、准确地恢复。

相机位置修正 ​

同时还完成了:

text
Capture Camera 使用双屏中心 X=0
Display Camera 保持 1P 中心

最终完整圆形下屏恢复,额外 Unity 元素轮廓消失。


十七、v0.5.0 最终功能 ​

当前版本已实现:

text
F10          开启/关闭下屏调整模式
方向键       移动下屏
+ / -        放大或缩小下屏
F9           输出当前参数与渲染管线信息
Enter        保存配置

同时支持:

text
RenderTexture 下屏重渲染
上屏保持不动
下屏独立调整
显示层与捕获层隔离
动态 Camera 检查
圆形遮罩诊断
纯色替换诊断
完整渲染管线输出

当前最终 DLL:

text
SinmaiScreenAdjustLite.dll
Version: v0.5.0
SHA256:
01F27CF255E2C872A5DF04A664B99F032CEDAD17BE1093CFBCEDE1A01EB8809B

当前安全基线配置为:

ini
SolidLowerColorTest=false
DisableOriginalCamerasAfterSetup=false
DumpRenderPipeline=true
CircularLowerMask=false

也就是说,保留完整诊断日志,但关闭所有会改变正常画面的测试选项。


十八、最终结果 ​

这次调试最终完成了最初目标:

  • 不加载完整 AquaMai;
  • 避开 Shim 与 PacketUpsertUserAll 的兼容性崩溃;
  • 保留 Sinmai-Assist 与 inject 启动链路;
  • 只调整游戏内部的下侧圆形屏幕;
  • 不影响上侧矩形屏幕;
  • 支持运行时实时移动和缩放;
  • 支持保存调整结果;
  • 修复额外 Unity 元素轮廓显现;
  • 完整恢复圆形下屏。

完整的问题演进可以概括为:

text
AquaMai 完整版崩溃
→ 尝试外部窗口缩放
→ 发现目标应是游戏内部下屏
→ 直接缩放 Canvas/Main,误伤信息栏
→ 改为 Background,仍不是完整画面
→ 使用 Camera + RenderTexture 重渲染
→ 出现额外 Unity 元素轮廓
→ 增加 Layer 隔离和诊断开关
→ 发现 uvRect 采样区域错误
→ 修正 RenderTexture、uvRect 与相机中心
→ 完整圆形下屏恢复

十九、调试心得 ​

1. “能看到变化”不等于“找到了正确对象” ​

Canvas/Main、Background 和最终显示画面看起来相关,但它们只是渲染链路中的一部分。直接修改它们虽然会产生变化,却无法保证调整的是完整下屏。

2. 不要把窗口调整和内部画面调整混为一谈 ​

外部 PowerShell 可以调整整个 Windows 窗口,但不能单独调整游戏内部的下屏。真正的内部屏幕调整需要进入 Unity 渲染管线。

3. Mod 显示为已加载,不代表它运行正常 ​

只要日志中仍存在:

text
ReflectionTypeLoadException
Missing dependencies
BadImageFormatException

就不能认为 Mod 已经处于可靠状态。

4. 局部覆盖包很容易制造混合版本环境 ​

关键 DLL、主程序、资源文件和 Mod 必须来自匹配的版本。更新后应保留文件清单与哈希,而不是依赖文件时间和文件名判断。

5. 空目录也是程序依赖 ​

option 目录缺失导致 inject 无法启动,是这次最容易被忽略的问题之一。部署和备份时应检查必要目录,而不只是文件。

6. 诊断开关比继续猜测更有效 ​

纯色替换、禁用原始相机、渲染管线输出和圆形遮罩测试,将模糊的视觉异常拆分成了可以验证的路径。

7. RenderTexture 的尺寸与 UV 采样必须成套设计 ​

本次最终问题并不是简单的 Layer 或 Mask,而是采样区域本身不正确。只有确保下屏采样区域是正方形,后续缩放、遮罩和位置调整才有正确基础。

8. 最小化实现比修复完整 Mod 更可控 ​

完整 AquaMai 包含许多与屏幕调整无关的模块。将需要的功能提取为独立 Lite Mod,可以减少兼容性风险,也更容易定位问题和维护。


二十、后续维护建议 ​

当前 v0.5.0 可以作为稳定基线。后续改动应尽量围绕现有显示管线小步进行,不应再次大幅重构。

建议的后续功能包括:

  1. 增加一键重置 OffsetX、OffsetY 和 Scale;
  2. 保存配置前自动生成备份;
  3. 增加 1P / 2P 显示选择;
  4. 支持上屏独立调整;
  5. 在启动日志中输出最终生效配置;
  6. 改善 F10 调整模式中的参数显示;
  7. 增加配置合法性检查和范围限制;
  8. 为不同分辨率建立独立预设;
  9. 保存 DLL、源码、配置和哈希清单;
  10. 将调试版与日常使用版配置分开。

结语 ​

这次问题表面上是“下屏圆形画面无法对齐”,实际涉及:

text
游戏程序集兼容性
MelonLoader Mod 依赖
启动脚本
必要目录
摄像头配置
Unity 对象层级
Camera
Canvas
RenderTexture
Layer
Mask
uvRect

最终最重要的认识是:

屏幕位置调整不是简单移动窗口,也不是随便缩放一个 UI 对象,而是对游戏内部显示管线的重新组织。

通过把 AquaMai 中真正需要的显示逻辑提取成独立 Mod,我最终在保留现有启动和输入环境的同时,实现了下侧圆形屏幕的独立位置与缩放调整,并避开了完整 AquaMai 带来的兼容性问题。

内容以研究记录和项目文档为主