概要:在过去的一段时间,Saigetsu由于自己的兴趣,进行了一些 东方凭依华 的二次开发,。本文尝试通过对二次开发内容的描述,发掘出一些有趣的技术细节。

实现了什么

基于 s-yukikazeHaushaltsbuch,实现了东方凭依华的replay自动轮播功能,并应用于24小时无间断直播中。为了更好地进行直播,同时也对东方凭依华的replay进行了解析,将对战双方的ID显示了出来,并支持了部分直播间弹幕控制功能。

如何实现的

为了实现这样一个功能,我们可以将需要实现的分为以下几个方面:

  1. 游戏状态的获取
  2. 控制信息的发送
  3. 直播相关的支持

下面我们将对每个方面分开进行说明。

游戏状态的获取

如果要对一个游戏进行操控,准确的获取游戏的状态,自然是最基础,也是最重要的部分。黄昏Frontier东方非想天则 之后,采用了一套新的界面显示系统(SQVM)来进行显示部分的渲染。可能是为了开发人员与美术设计人员的解耦,开发效率的提升,SQVM实现了一套脚本系统,来管理游戏的界面元素。

说到脚本,最先想到的就是以字符串形式存储的各种标识符,相对灵活的伪对象特性(就像javascript那样!)。SQVM中就以类似的形式,在运行时维护了一个树形结构,将显示相关的各种游戏状态量保存在了里面,可以以目录类似的字符串形式进行访问。

于是,我们可以通过获取脚本中指定变量的信息,来得到游戏所处的状态。此处具体的获取逻辑,是使用了 Haushaltsbuch 中对应的部分。它将一个dll通过Windows消息循环的钩子,注入到东方凭依华的游戏进程中,然后创建一个隐藏的窗体,然后通过运行消息循环来实现IPC机制,最后借助ReadProcessMemory读取具体的游戏信息(需要注意的是,具体的数据传递,是通过dll文件中一个专门的数据段实现的,而不是消息传递)。

控制信息的发送

在获取到了具体的状态信息后,就可以根据当前的状态,进行模拟输入了。要实现像按键精灵这样的模拟输入,一般有两种方式:一种是通过 SendMessage 将按键的按下发送到对应窗口的消息循环中;另一种是通过 SendInput 这个API来模拟键盘的输入。前者可以在非焦点的情况下使用,而后者可以用于大部分游戏中,也可以更加精确地进行控制(按下的时间长度之类的)。这里采用了 SendInput 来进行控制按键(上下左右,Z,X)的输入。

需要注意的是,在游戏中是每一帧检测一次按键的按下情况,所以在发送按键信息时,必须设定为按下一定的时间,而不是一瞬间,后者在大多数情况下,无法被游戏检测到。同时,“上下左右“四个键位属于扩展键位,在模拟的时候需要进行特殊的处理。

由于在游戏中,状态的切换有时不是瞬时的(如凭依华的主菜单,连续按下若干下,与持续按下效果是不同的,而如果模拟的输入指令调用过于频繁,可能会被识别为后者),我们的模拟输入也需要有反馈机制(发送输入后,检测状态是否已经对应改变),使得整个操作过程是需要等待的。如果采用基于轮询的同步阻塞机制,会导致UI线程的阻塞(此处需要注意的是,协程机制有一种实现是把将要执行的函数上下文,丢到消息循环之类的队列中,在消息循环等合适的时候取出执行,以保证是在UI线程中执行的。这也意味着通过这种方式依然不能承载同步阻塞的代码),于是我们需要将操作代码放到新的线程上执行。

在PYHHelper中,我采用了C#的Task和async/await机制,将状态监视函数包装成为了一个Task,在执行操作的async函数中,可以通过await进行低代价的等待,也保证了在代码上的连贯性。

直播相关的支持

对于这样一个24小时的直播间,首先需要实现的就是异常状态的处理,即如果控制信息等没有成功奏效的处理方式。东方凭依华在设计的时候没有完全解决内存泄漏的问题,在连续运行8小时左右会产生约700MB的内存泄漏,之后因为内存alloc的问题崩溃。在replay轮播模块,则需要检测到游戏的崩溃,并且在异步操作中检测到游戏的崩溃以终止此次操作,最后重新将游戏启动。

我机器上的东方凭依华是通过steam启动的,要启动凭依华自然得通过steam的方式。Steam提供了通过自定义url来启动游戏的方式。在浏览器中访问“steam://run/游戏id”这样的网址,正在运行中的steam即会收到消息打开游戏。在我们的程序中,也可以通过ShellExecute这样的地址,来向steam发送信息。

同时,为了保证直播间的互动性,观众可以在直播间输入IP地址,然后PYHHelper会操纵游戏自动进行观战。在具体的实现中借助了B站弹幕姬的插件功能,制作了一个插件获取弹幕信息,然后将具体的操作通过 剪贴板+SendMessage 的方式发送到PYHHelper主程序一侧。

总结

从具体应用到的技术上来说,这样一个程序也可以定性为简单的API堆砌吧,以需求为导向,将需要的各种库组合在一起,自己原创的部分只有少数的胶水逻辑。不过在实现过程中,也遇到了许多细节问题,从这些细微的地方,有可以进一步的领会原理上的东西。如果有时间,去做一做这种东西也不错呢w。

杂项

一些不知道放在哪里的内容就写在这好啦w!

  1. 自动replay拉取

实际上为了保证直播的内容一直都有变化,每天都会自动从Tenco!上拉取各位玩家上传的对战play,用一下http接口就好的事情啦!拿到网页内容,然后靠正则表达式把replay的url都匹配出来什么的~~

  1. 头像&玩家ID支持

切换replay的时候顺便解析一下replay的事情。对replay的解析参考了tako774的 东方凭依华上传工具 的代码。在replay中专门有一个部分是战斗的Metadata,记录着双方玩家的名字,使用的角色信息,胜利方等等。通过Deflate进行了压缩。拿到之后就可以匹配头像/显示玩家id了。记得对Shift_JIS进行转码!

  1. 压缩方式相关

Deflate和zlib是有区别的!!!!zlib多了一个文件头,略过之后就可以丢给Inflate了。

  1. ProfileUploader相关

本来想着顺便做一下凭依华战绩上传等工具的汉化的,不过改完之后再编译的时候发现,自己无论怎样也无法配置好Ruby编译成exe的开发环境(这里感谢一下 @taroxd 在ruby代码阅读方面的帮助),于是一怒之下自己重写了一个!

  1. 点歌姬的逆向

想给自己的直播间加上自动轮播bgm的功能,但是点歌姬自己呢,是没有带上对播放列表进行播放的功能的,于是尝试逆向了一下点歌姬。令人悲伤的,点歌姬的代码通过混淆工具进行了处理,完全看不出脉络。不过通过插件通用的Interface,找到了一个插件的单例!最后就通过从弹幕接收的接口模拟发送弹幕,实现了播放列表的功能w。(这里安利一下自己做的网易云音乐API库(醒醒你作为一个API库的文档都没写好就别宣传了))