第二代红警看戏神器是如何实现的
第一代看戏神器在2022年8月推出,作为试水产品,反响不错,但有明显问题。本次全新重制,使用方法、技术原理均不同。
实现放大器的两条路线是实时方案和后期方案。具体参考

第一代采用了后期方案,需要先录制视频,再将视频传入软件,得到放大后的视频。它开发相对简单,但操作麻烦、耗时,不能用于直播。
第二代采用实时方案,在游戏过程中实时放大聊天消息,录制的素材自然也是放大的,且能用于直播。该方案效果最好,但开发最难,因为要考虑到和红警客户端的“嫁接”,用今天的代码和20多年前的游戏对话。相比之下,后期方案只要做视频处理,改进到头也就加个OCR,都是通用技术。
本次的看戏神器,整体思路是做一个抓包程序,一旦发现红警聊天数据包传输,就使用一个悬浮的透明窗口,同步绘制出其中的文字。
首先,要证明,自定义的内容可以叠加在红警的游戏画面之上。桌面歌词能悬浮在各种窗口上,但红警中看不到,这代表游戏画面优先级非常高,几乎没有其他视觉元素能浮在红警的上方。打游戏就应该把其他窗口后置,当然没问题。但这个习以为常的现象,给开发带来了难题。
我首先想到的是虚拟准星。很多硬核FPS游戏没有准星,但GitHub上有很多准星应用,专门负责在屏幕中央画一个准星,可以自定义颜色、大小,可以悬浮在游戏画面之上。稍加改造一下准星的代码,把绘制准星改为绘制文字,不就解决了吗?

如果将红警窗口化呢?理论上可以,但非常影响游戏体验。鼠标移动到屏幕边缘时,会掉到游戏外面。任务栏也会影响视野,就算自动隐藏任务栏,鼠标移动到屏幕下方,也会把任务栏召唤出来。每次鼠标掉出去,会导致游戏短暂失控,肯定不能忍。
那么,Windows系统里有什么能凌驾在红警之上呢?我发现了4个,音量调节数值条、大小写切换的提示图标、Xbox悬浮界面、显卡弹出的提示。至于显示器下方那一排物理按钮触发的弹窗,不属于操作系统,无法开发,不做讨论。

我也试了OpenGL、GDI+,但它们也运行在窗口实例中,并不能直接在屏幕上绘制像素点。所有窗口都不能悬浮,能悬浮的元素又不开放接口。这个问题不突破,实时方案永远无法实现。于是我就鸽了。
直到3月初,我又想起来这回事,查资料看看有没有新的解决方案。发现红警渲染器cnc-ddraw在2023年1月17号发布了5.0,这不正是对战平台的默认渲染器吗?放到岩浆里热热还能吃!
我用新版渲染器覆盖了对战平台的红警客户端中的渲染器,电脑瞬间闪出万道金光(误)!稍加配置,红警的行为就像正常窗口一样了,甚至网易云都能悬浮在它上面。这样就妥了,做一个背景透明的、置顶的、不能被点击的窗口,就可以作为聊天消息的输出端了!我对比了几种文字绘制的效果,OpenGL语句太复杂,而且有点大材小用;GDI+在透明背景上,开了抗锯齿,文字有白边,不开就有锯齿,简直简直了!最终选取了PyQt,文字造型非常之优美。
我没咋用过PyQt,所以剩下的工作就交给AI了,我负责提需求和拼装代码,它写具体业务代码。别看AI聊社会人文有一些空话套话(当时用的GPT3.5),写代码是真不含糊,优雅命名变量,举一反三,贴心解释,妈问跪。对于有想法的路人,写代码的门槛进一步降低,懂点编程,能拆解需求,就能亲手实现一个创意了。对程序员,能通过和AI对话,快速学习新的语言或开发框架。我很看好此事,毕竟我当年也是为了实现某创意,被迫写代码的,这又是另一个故事了。
首次启动看戏神器,全盘搜索包含gamemd.exe的文件夹,将其视为游戏文件夹,注入新版渲染器及其配置,同时缓存这些路径。由于使用了os.scandir()方法,广度优先,且限制搜索深度为3层(没人会把红警安装在一个特别深的文件夹里吧,不会吧不会吧,又不是学习资料),搜索速度很快,几乎感知不到。每次启动神器的时候,也会校验一遍渲染器。
绘制时,让每条消息停留8.5秒,方可和游戏中同步。游戏速度不影响聊天消息的停留时间。
拦截
在开发第一代的时候,已经有了相关积累。我知道聊天流量包的特征:UDP协议,长度为483字节。如果聊天内容不满483,也会发来483的包;如果超过483,会在客户端输入的时候自动截断。
但通过ip.addr(WireShark的搜索语法)很难判断,因为对战平台的IP有很多,每次对战都会变。本地IP和端口也因人而异,不排除有直接拿公网IP玩游戏的用户。
于是只能通过UDP和483字节这两个线索,制作拦截器。为了减少不必要的监听,使用父子线程方案。父线程监听红警客户端进程,子线程监听网络包。看戏神器启动后,先启动父线程,轮询红警客户端进程是否启动。若是,启动子线程,监听长度为483的UDP包。一旦父线程发现红警客户端关闭,子线程结束监听。
可以通过进程名称,确定游戏客户端是否已启动。无论原版还是尤里,进程名都叫做gamemd-spawn.exe,都是尤里的紫色图标。实际上,每次开始游戏,都会创建一个新的gamemd-spawn.exe,全新安装的红警没有这个文件。上文提到的gamemd.exe则是游戏客户端的本体,全新安装的红警也有,且不会随着每局游戏发生变化。
上述拦截逻辑,意味着网络请求过多时,有可能延迟或遗漏,所以神器开启后会弹窗提醒
虽然我觉得已经没人用迅雷了,不过大家一听名字就知道是下载器,还是写上去吧(doge)
解密
开发第一代的时候尝试过,奈何数据包都是加密的。大神给了我一段代码,说来自国外论坛。我稍加改造,支持了中文解码(红警用了UTF-16编码中文)。解码之后,我发现之所以聊天流量包永远是483字节,是因为长度不足的时候会填一堆空格。
但是,聊天流量包不含玩家颜色信息,只有玩家序号和聊天内容。该如何绘制出不同颜色的文字呢?大神再次出手,告诉我每局游戏开始前,都会在游戏根目录创建spawn.ini,其中包含了每个玩家的序号、昵称、颜色枚举值。
这意味着我们可以建立两份关系,序号和颜色、序号和聊天信息。这样一来,拿到颜色和聊天信息的关系,也不在话下。
枚举值是纯数字,不能一眼看出这个玩家是什么颜色。数字和颜色的对应关系,应该藏在红警客户端中。
于是我使用XCC Mixer挨个翻找客户端中的mix文件,找到了rules.ini,其中包含多处对颜色的定义。MP应该就是multiplayer,即多人游戏。第3-4行的注释表明这是HSV而非RGB色值,而且H、S、V的取值都是0-255,这场面我真没见过,不知道是不是他们自己发明的规范。所以又让ChatGPT帮我写了一个颜色转换方法。

第31行的注释表明,这段代码由一个叫“PCG”的人写于2000年9月10日,如果TA当时是个20多岁的程序员,现在已经年过半百。
这里的枚举值和颜色并不完全对应,但色值都是准的,稍加改造就得到了这样一份枚举。

每次游戏启动,就会检测游戏进程文件夹中的spawn.ini,一旦有了有效内容(需要等几秒),就分析出每个玩家的序号和颜色的对应关系,暂存起来。有聊天消息时,结合聊天消息中序号和聊天内容的对应关系,便可绘制出正确颜色的文字。游戏结束时,销毁暂存的序号和颜色的对应关系,免得下一局颜色串了。
