《素晴日》移植笔记

写给学校动漫社社刊的移植笔记_(:з」∠)_

移植就是把在PC上玩的Galgame游戏脚本和资源文件进行拆解和处理,转换成移动端支持的格式的蛋疼工作。移植游戏里应用最广泛的引擎是Onscripter,此外还有pymoRen’Py等,这里的《素晴日》移植就是用Onscripter实现的。关于Galgame是什么,可参见这篇长微博http://ww1.sinaimg.cn/bmiddle/4e627219gw1ediuv1sdvfj20hsaze1ky.jpg(看了似乎更不明觉厉);移植是什么,还有移植贵圈真乱的破事,可参见http://tieba.baidu.com/p/2282820919,此处都不再赘述。此文目的是供观赏,让没有尝试过的同学知道有这么一回事,能供有兴趣的同学参考上当然再好不过了,因此本文尽力避免大段细节叙述而又希望做到言之有物。接下来就开始正题(xiache)了。

 

1. 前期工作

       前期工作包括确定游戏使用的引擎,解包资源,查找前人对这种引擎的破解工作等等。做好一个Galgame的移植需要知道哪些东西呢,游戏脚本语言?汇编?数据结构?不知道也完全没关系(我就是这样),边做边查找资料即可。实际上大学课程也一样,学到的也是一个索引,以便于具体要用时能知道需要查找那些东西。

       打开游戏目录,会看到一坨.arc文件,还有启动程序BGI.exe等。此Gal引擎名就是BGI,算是最常见的Galgame引擎之一了,有很多前人的工作和工具,善于利用可以少走很多弯路。在一无所知的情况下查找资料,就得善用搜索引擎,捕捉信息。搜索也是个技术活呢。

       澄空学园的Galgame汉化破解讨论区有较多的探讨,acgf上有移植引擎的教程。此外还有一些英文网站或博客,如asmodean.reverse.net有各种引擎的解包源代码,tlwiki.org上有一些Galgame脚本的反编译工具等等。

常用的解包工具有Crassarc_conv等,此处用的arc_conv,把目录中dataxxxxx.arc都拆掉,得到一堆脚本、图片、声音文件。接下来的工作就是对这些文件分别处理了。

 

2. 压缩素材

  Galgame本身通过光碟销售,图片基本上都未经压缩,很多游戏的声音都是用的无损的wav文件,而爪机空间有限,另外也是为了下载方便,故需要大幅压缩素材。立绘通常有数千张,语音文件有一万多个,所以写批处理脚本是必须的技能。

  data02xxx.arc中为图片,解出来均为tga文件,这是一种高质量的无损压缩格式,一共2G大,我们当然要压扁它。图片又分两种,位深度为24的真彩色图片,例如背景;带透明度数据的位深度为32的则主要用于立绘。作为有损压缩的jpg所占空间是最小的,jpg质量设为90时,对于二次元图已经几乎看不出与无损压缩的差别,而png的压缩级别开到最大(9)即可。tga2432位需要通过获取文件的第17字节识别。以下是一个批量转换的Perl脚本示例:

#Usage:查找目录下24位tga转为jpg,或32位的tga转为png
use File::Basename;
$indir = 'I:\Gals\素晴日解包';
$outdir = 'grp';
mkdir $outdir;
@files = <$indir/data02*/*.tga $indir/sysgrp.arc~/*.tga>;
foreach $tga (@files){
    open BIN,'<'.$tga;
    binmode BIN;
    $bname = basename($tga);
    $bname =~ s/^(.+)\.tga$/$1/i;
    read(BIN,$buffer,16);
    read(BIN,$buffer,1);
    $bit = unpack("H*",$buffer);
    if($bit eq '20'){ #32位tga
        system "G:/XnView/nconvert.exe –quiet -clevel 9 -out png -o out/$bname.png $tga";
    }elsif($bit eq '18'){ #24位tga
        system "G:/XnView/nconvert.exe –quiet -q 90 -out jpeg -o out/$bname.jpg $tga";
    }
}
  BGI引擎中按钮是一张图片横向分3栏,分别表示未激活,鼠标悬停和按下的按钮,很幸运,这与Onscripter的设计相同。而有些引擎图片分栏是纵向的,这就需要分割和组合图片了。Perl以其语法丑陋而闻名,不过因其高度灵活的语法,用于写用后即焚的小程序以快速地解决问题再方便不过了。移植的工作之一就是批量处理各种文件,之后还有很多步骤需要用脚本语言解决。此处批量转图使用的是NConvert,这是图片预览软件XnVIew中自带的一个程序,参数是临时要用时看help信息填出来的。对于这种命令行工具,看懂说明之后用起来还是很方便的。Onscripter做移植引擎的情况下,可使用ImageUtility这个专用于krkr/Nscripter图片处理的工具进一步压缩,将png转换为半边遮罩的jpg

  data03xxx中是音效,data05xxx中是BGM,码率都不太高,用的有损压缩压缩的ogg,总体积不大,原封不动地复制即可,当然音效还是有压缩空间的。data04xxx中是语音,码率虽然不高,但10000多文件一共近1G,压缩的收益是相当可观的。对于人物语音,采样率441kHz,码率32kbps就能听了,虽然音色有些变化,调到48kbps音色就能较好地保持。以下是批量转换语音的Perl脚本:

@files = <I:/Gals/素晴日移植/data04*.arc~/*.ogg>;
use File::Basename;
mkdir 'wav';
foreach $file (@files){
    my $bname = basename($file);
    $bname =~ s/^(.+)\.png$/$1/i;
    system("ffmpeg -loglevel error -y -i \"$file\" -vn -acodec libvorbis -ar 44100 -b:a 32k -f ogg wav/$bname");
}
目录中有5arc档没有被解包,它们实际上是mpg格式视频文件,用播放器可以直接打开。Android平台的Onscripter支持AVC编码,但iOS似乎不支持。总之,这里将视频转成了视频AVC编码,音频AAC编码的mp4,这里使用战B站常用的小丸工具箱,800x600分辨率的视频,crf23.5,使用2pass将视频码率控制在930kbps,音频码率72kbps,这样能使总码率小于1000kbps,能大幅压缩臃肿的mpeg2视频,并且对质量影响微小。声音的转换这里使用的是ffmpeg,这是一个广泛使用的音视频处理软件。使用vorbis库转换ogg档码率,目标码率为32kbps

 

3. 脚本解析

BGI引擎的解析可见http://bbs.sumisora.org/read.php?tid=11042351,这也是一篇十分明了的汉化破解教程。

  这里使用了http://tlwiki.org/index.php?title=File:Bgi_asdis.zip&上的BGI反编译工具将二进制的字节码脚本反编译成类似于汇编的文本脚本,我们的工作就是将下图(2)类似于汇编的文本转为下图(3)Onscripter语法的脚本:

(1)二进制脚本:


(2)tlwiki的工具转换成的类汇编脚本:


(3)Onscripter脚本:


有了tlwiki的工具,分离了指令,解析出了+-x/等操作符,省去了解析二进制脚本的步骤,使移植方便不少。Fate/Stay Night的移植者skydark拆秽翼时将此工具改写,直接导出Onscripter脚本,但是对素晴日不工作,还是用Perl大法来处理文本吧(实际上是自己python水平太渣调不好前人的程序)。

  翻译脚本的过程相当于写一个解释器,但不是像游戏引擎根据字节码进行绘图操作,而是导出Onscripter脚本。指令都是操作符(操作数); 的形式,用正则表达式 (.+)\((.+)\); 匹配行即可获取。理论上也可以直接用蹩脚的Onscripter语法完成这些工作,被视为移植游戏之巅峰的Fate/Stay Night的移植版就是用Onscripter实现了krkr语法的解释器。但是自己没有这般功力_(:з))_,所以还是用更方便的脚本语言把基于栈操作的表达式运算翻译成中缀表达式,把标签、跳转、变量赋值都翻译成Onscripter语法形式,其它函数则翻译成“f_xxx 参数列表”Onscripter宏调用的形式。这就是脚本解析要做的工作。

  操作过程中会遇到很多问题,例如加法操作需要栈中有两个元素,有时演算时只有一个了,原来是之前的某个opcode会压栈;有一个参数有时传递的是图片文件名字符串,有时又是数字0,而Onscripter自定义指令不支持重载,就得甑别出有重载的函数,分别命名为f_xxxf_xxx_1等。有些移植游戏文本中出现乱码,甚至闪退,多半就是粗心的移植者忘记转换半角字符导致的,这些问题都需要不断调试不断修补。函数参数性质的统计也是用脚本语言完成,并不断修改,直到导出一个自定义指令定义表,而这些指令的实现就是下一步工作了。

  脚本解析中另一项大头就是文本的处理。Onscripter有一个糟糕的特性就是只能显示全角字符。例如字母“A”只占汉字一半长度,编码只占一个字节是半角字符;而这个字母“A”与汉字占同等宽度,编码与汉字所占字节数相同,这就是全角字符。游戏中的英语,数字,颜文字等含有半角字符,需要转换;显示首发邮件是一长串的下划线也是半角字符,为最大限度地保留原排版,只能将两个下划线替换成一个全角横杠。还有神烦的转义字符,这些都需要在脚本解析步骤要处理完毕。

sub word_halftofull{ #字符的半角转全角, UTF-8编码
    my $ch = $_[0];
    if($ch eq ' '){
        $ch = chr(0xe3).chr(0x80).chr(0x80);
    }
    elsif($ch =~ /[!-~]/){
        my $uni = ord($ch) + 0xee0;
        my $first = 0x80 + int($uni / 0x40);
        my $second = 0x80 + $uni % 0x40;
        $ch = chr(0xef).chr($first).chr($second);
    }
    return $ch;
} #未考虑半角假名的情况,转为GBK编码会变成???,暂未找到好的解决方法_(:з)∠)_


  脚本解析完成后,会得到两个文件:剧本和一堆空的自定义指令。接下来的工作就是实现自定义指令,遇到难以处理的问题时,剧本的导出可能还需要返工。因此第3步和第4步的联系是比较紧密的。

4. 脚本实现

  如果破解工作已有前人做完,这一块算是整个移植流程中最费神和最难做好的了。上一步最后我们得到了一大堆空函数,就像这样:

*f_3c7
    getparam  %1001, %1002
    return
 
*f_3d0
    getparam  %1001, %1002, %1003
    return
  我们需要一个一个地实现。听起来长路漫漫,事实上也是如此。素晴日中有一百多个这样的指令,其作用只能靠猜出来。指令可按功能划分为以下几类:

  ①变量操作

  记录事件的触发,HScene是否已读,某条线是否打通等等,对于有好感度系统的Galgame,还用于操作好感度。需猜出哪些是临时变量,哪些是全局变量。BGI中还有一个全局的堆空间,移植素晴日时采用了取巧的方法实现:扫描一遍哪些偏移量被使用过,然后直接声明一些名为base_offset_xxx的变量,不然要手动管理大小为数千的数组(这是投机取巧的方法不要学我)。

  ②选项控制

  选项控制就是游戏中选择支的实现。OnscripterBGI选择支实现的形式很不一样,但人类的智慧是无穷的,使用取巧的方法+不断尝试,总能整出来。况且选项不多,检查也比较容易。自己使用的取巧的方法对于选项数目在10个以下时没有问题,结果测试时发现素晴日这猎奇游戏有一处竟然有60个选项,占满了整个屏幕。还有序章推完后选择章节的界面,判定碎片解锁和悬停显示都需要手动实现,等等。

  ③文本显示

  这一块在上一步骤中已处理完毕,原因是Onscripter字符串处理太弱,不如用文本处理力气的Perl先行解决。现在已有一些跨平台的新引擎,例如基于Python语法的Ren’Py,也可以使用python的库;面包工坊的新引擎BKEngine也自带正则表达式类,虽然此引擎还存在较多Bug,文档也暂不完善。

  ④声音

  这一块是相对简单的。参数也比较容易猜得:文件名,是否循环,通道号,淡入淡出时间,不外乎就这么多。

  ⑤图片

  图片显示又算是脚本实现中最为困难的部分了。不同引擎的图片显示方式不大一样,图片的显示方法也有多种。是立即显示,还是等待画面刷新?切换时是独占还是可以同步?因Onscripter是单线程的,同步的图片移动必须借助Lua扩展。由于Onscripter对特效的支持十分羸弱,移植时不得不做出调整,或阉割掉一些特效。图片包括背景,立绘,CG,素晴日中这三种图片涉及的指令不尽相同,坐标的计算方法,显示和消去的时机都不一样。例如,脚本中有显式的消去立绘的指令,但更多的情况是切换场景时立绘自动消去了,但是添加新立绘的指令却出现在切换场景的指令之前,这就奇了怪了。最终再次采用面向巧合编程的方法,对每一个显示的立绘设置一个状态标记,新出现时置为0,经过文本显示后置为1,转场时消去所有标记为1的立绘。又例如立绘的坐标是以画面下中为原点,取到图片下中的相对坐标,移植到Onscripter时必须换算成画面左上到图片左上的相对坐标。CG又分两种情况,一种是画面中心到图片左上,另一种似乎是画面中上到图片中上,如果CG有缩放演出,坐标的猜测就更难了。直到现在移植出的成品还有少量CG显示位置错乱,就是因为在缩放上遇到了麻烦。

  还有其它指令,包括等待一定时间的流程控制,还有一些计算例如取随机数等。游戏的演出正是由这些基本的指令一个一个累积起来的。而移植的工作,就是各种猜测,各种面向巧合编程,我们不求写出正确的代码,只求代码能正常运作即可。

 

5. 系统实现

  前面说了那么多Onscripter的坏话,但此引擎最大的优点就是简单,有默认的存读档界面,适合懒人(比如我)。只需要标题界面,心情好可以实现鉴赏界面。按钮素材的坐标确定可以用Adobe Fw。系统的实现需要手动码脚本,参考前人的移植游戏中系统的实现方法,难度不大,就是比较费时罢了。

 

  于是,一个移植游戏就完成了,虽然花的时间似乎都够推一遍素晴日,但在实践中熟悉了Perl,又补了PythonLua,对汇编的指令有所涉及,解析类汇编的脚本后都有种自己也能写出一个Galgame引擎的幻觉,算是收获不小了。面向解决问题的学习的效率是很高的,未尝试时觉得这何其困难,而现在想想也就那么一回事。