这是一篇debug notes,我有很久没有记录debug notes。今天发生了什么样的事情呢?我的目的是要修改一个游戏,游戏叫刀剑江湖路,还在EA,玩家还不算太多。那么要修改游戏,首先是不是走动态内存修改比较方便呢?那肯定是的。那我的问题是我跑在我的macOS,并且是Apple Silicon的mac OS里面的Parallels虚拟机里面的Windows,那么在这样的情况下,内存修改它往往会遇到一些问题,有权限问题,有内存的mapping问题等等。
OK,那么拿到游戏我首先试的当然就是Cheat Engine,然后会发现它读不到。它读不到的具体特性是它能读到最外层壳的EXE的一些DLL,但是它读不到实际游戏运行的大部分内存。那么在这一步一般会检查以下几个事儿,会去把Core Isolation,关掉,这个是Windows的Core Isolation,就是一系列的安全性限制。什么管理员权限这些不用说,会把Windows Defender关掉,那个real time threat detection之类的东西关掉。
会把Hyper-V考虑关掉,其实Hyper-V对虚拟机性能还是很有帮助的,但是它应该指的是在它里面跑虚拟机。不管怎么样,Hyper-V来关掉也对它那个内存mapping有些帮助,以及其他的一系列措施,什么Test、Sign、Drivers等等,但是还是不生效。
这个情况是比较常见的,在虚拟机里面跑的这个内存修改就是会遇到有时候很难解决的问题。而且实际上它不是所有的内存地址都读不了,它能读一部分,这个就更加棘手一点,然后又试了——当然了你会说找个现成修改器,这个游戏玩的人不是特别多,有现成的一个修改器,然后它不生效——可能性主要有两个,第一个也是内存mapping问题,第二个是游戏版本和修改器版本的问题,比较倾向于是前者,但不管怎么说,这两个内存修改的路子都断了。那接下来怎么着弄呢?那就是存档修改,存档修改会稍微麻烦一点点,但也是完全可以做的。那在这个过程中我就想了一下,先瞟了一眼它的存档文件,是个3 megabyte的大文件,那也就是说它的存档文件大概率是把游戏状态整dump了进去,3MB还是比较大的,那这个修改起来检索起来都会不太方便啊。
然后我先是file一下,看它文件类型,那果然识别不出来,就最外层不是一个container的archive,也不是什么media或者text,是给的是data。那这个时候我就有点烦了,我觉得要搞清楚这个文件的结构,说不定要一晚上,然后我就想别的路子,心想我跑一台proper Windows怎么样呢?找个云端的机器,于是这个我就随手一搜,以前最早用过AWS的Cloud PC,那体验是非常差。然后我再搜呢,专比较专门的这种Cloud PC,Azure当然提供,然后Windows 365提供,还有一家叫ShadowPC。它在它的宣传里面更偏向于Gaming purpose。我就先起了一个Windows 365,希望它能work。然后在起的过程中,这个机器发现我发现它很慢,它准备这个机器可不像一个EC two instance,它要准备反正是几十分钟。那在这个过程中我就随手把ShadowPC也开了。
好,然后Windows365的机器好了,我就先远程到这个机器上,有一些不便利,但是还算正常,比如说一些键盘快捷键他都考虑到了,我没有Windows键,他就给我了一个Mapping等等。但真正的问题是Windows365这个网络非常卡顿,受不了。我从伦敦——伦敦也谈不上是网质量最好的地方,我的酒店的这个Wifi一般,但是它的卡顿程度是非常惊人的。把IP看了一下,IP是日本IP,这个挺ridiculous的,不知道他怎么想的,给我分配一台日本的机器。那这个时候我就把Windows 365丢一边关掉了,然后我打开shadowPC,它的连接质量会好一些。然后我成功把这个游戏装上了。但是还是有很多不便利的地方,比如说我要上传文件,下载文件,你看我需要在这个远程PC里面装Steam之后再登录,登录完了之后装我的游戏,然后再装内存修改工具,那在这个过程细节很多,比如我登录Steam,他会说你这个是可疑登录。因为这个位置和我完全不搭,他在WashingtonDC,我在伦敦,我不知道他为什么给了我这样一台机器,可能因为我机器的什么原因,他默认我是在美国这个位置。好,他给了我一台DC的机器,我从伦敦连比较远,然后Steam会觉得它是可疑登录,我就要想办法去验证,让它能够登录成功。登陆成功了之后我要装我这个游戏,装游戏从它那个数据中心来装,当然非常快。那另一步我要装内存修改工具,比如说我要装Cheat Engine,如果你其实我是买了Patreon的,我每个月是给Cheat Engine付钱的,所以按理说我是有一个无广告版可以装,但我的无广告版需要我登录我的Patreon账号来证明我是我给你捐过钱,对吧?但是在这台远程虚拟机里,不要说登录找密码账号很麻烦,就光是它要Cloudflare验证我是不是human,我就有点过不去。因为这是一台Cloud PC,它很可能在数据中心IP,所以说我打开Patreon想登录的时候,它就会拦住我,给我CAPTCHA,然后这个CAPTCHA又很难——什么识别图中的bus、识别图中的bicycle,那我就要去装这个有广告版的Cheat Engine,然后我就要去下这个有广告版的Cheat Engine的安装文件,这些都是小小的摩擦,挺烦的。然后如果要是登陆风灵月影,因为不是有两个内存修改的路径,这个风灵月影也要登录账号才能用。然后它是收一个验证码,收验证码那个手机号在我另外一台手机上,它当然也不支持这个非86的手机号,所以麻烦多多。在这个过程中我就退回来,心想算了。为什么算了呢?因为你想我拿这个Cloud PC我是没法玩的,他肯定太卡了。这是一个战斗,比较像DNF那种2D有一定动作元素的战斗,我是不可能在卡顿的情况下打的。所以说我在这边就算做好了修改,我要把这个存档文件在保存好,就是你在内存里修改,然后你再存储进度,然后你要把这个Steam云端同步过来,对吧?然后这个是一轮修改,然后你可以在自己本地的虚拟机上玩。玩的过程中如果你又要涉及到修改,你要回到你的Cloud PC,你要再同步这个存档。不管你存不存档,是用Steam cloud来同步,还是说找个方式从那个Shadow PC里面直接把文件拖出来,这个都摩擦太大了,我心想我还是搞一搞存档文件吧,这能有多难呢?这个有时候工程上的事,就怕你这个自信心太强。都不难,但是都够你喝一壶的。
好,那我们就来看存档文件,我就把这个Shadow PC也给退了,回到本地我们来看存档文件。那么这个明明白白是个Unity游戏,然后人家也没有去买Unity的什么license,所以说开头就是Unity的logo,没问题。Unity文件,刚才讲了,这是一个大概3个MB的存档文件,我file了一下不知道它是什么。那我接下来怎么办呢?就打开吧。打开的这个时候我犯了一个错误,我没有上来用xxd,然后再加head这种方式去看它头尾,我是直接拿这个VS Code去开了一下,VS Code里面也有一个插件Hex Edit,然后打开来的这个文件我就一下没关注他那个头,他那个头是可读的一个头,然后我就直接想办法去解压缩,那他人家肯定整体不是一个压缩包。然后意识到这个问题之后,我开始去看它头尾。尾部没有trailer,但是它的头文件是可读的,叫做——1个byte——key player start还是什么东西,key player state,然后接着MH什么什么东西。一看就是这个汉语拼音首字母,jh应该是江湖,然后有个m。总之,然后你再看第一个byte的长度应该是16,16进制16的话就是22,那么我就基本能猜了,这个第一个肯定是key length,对不对?后面是一个key,然后它的那个key是一共22位,这22位的构成是key player state,然后什么mmjh,然后2023,然后后面又跟了一个数,10005。那在这个之后呢?之后就开始就全是byte了。好,那这个头我已经可以解出来了。
那这个头解出来了之后,我就看后面这个文件,这里是又卡了一段时间的,但是最终我想起来了Unity它要存档有一个什么很默认的、很主流的做法,就是EasySave3——ES3这个文件格式。好,比较符合这个文件格式,然后我就去往这里靠,然后就随便——我在Python和JS里都找到了ES3的Parser,我肯定不想,也基本做不到在我的本地装C#的这个运行环境,那人家原生是C#,但是实现肯定也是有。好,那我就随便用这个ES3 Parser来想打开这个存档文件,我认为它是一个格式的文件,那果然打不开,parse不了。那么parse不了呢,就开始看可能问题出在哪儿。首先它抛的错是有点不直接相关的,他说这个没有什么sixteen byte alignment乱七八糟的,但是没有alignment导致这个的问题就往往是这种解压缩格式的问题,然后对齐的问题,然后加密的问题。
好,首先就来看加密,就是这个ES3 file是可以用一个key来给它做个简单的对称加密的,是不是这样的?如果是这样,那我岂不是头疼了?嗯,好吧,别的不说,先退一步,这后面的这个是到底是不是一个压缩文件?如果不是的话,那你在干啥呢?但是这个所以说先查了一下,很容易就是生成一个Entropy Graph。这个Entropy Graph如果你看这个Beats都很高,说明这个全都是信息量巨大的randomness很高,一定就是加缩包文件。它其实代表是说如果你这个文件里全是0,有大量的这个很格式化、结构化的东西,它的Entropy Graph就会低嘛。好,那首先能确定它肯定是有randomness的这样一个压缩过的文件,那么它有没有加密?如果它加密的话,基本就不好搞了,我也没法从runtime里面把它key挖出来,但是我会强烈觉得它没加密,然后我就翻了翻C#里面默认的话这个加密也是不开的。那很可能这个没加密,然后这个里面我就产生一个怀疑,然后我就看了它这个Python实现和GS的实现,就是ES3,这个ES3这个Parser的实现,我发现它怎么着都要解密。你给他,比如说Python来说,你必须给parse一个string,你parse一个empty string,它就用empty string来解密,而不是说跳过加密这一步。但是在C#里面默认又是不加密。而且这个中国游戏,中国开发者我觉得闲着没事儿不会给自己上一层加密的,这不是有毛病?所以我坚定的就猜测它其实是没加密。但是人家没加密,你偏要解密,所以就抛出错误了。
好,为了验证这个想法,我就看了它的这个格式,我直接想什么parser不parser的,我自己来吧。然后我就不要用它的那个ES3 parser了,我就看了一下它的格式的spec,你一上来是个keylength,keylength后面就是一个key,把keylength和key都扔掉之后,这个offset就来到了新的地方,我直接这个新的地方看它是个什么格式来解就好了。然后因为压缩文件、Archive文件有很多种格式,但是它们都是有标记的。你用file用什么东西就都是能猜出来它是什么文件,于是我就在这个key和keylength扔掉之后,后面的几个byte我印了出来之后,和那个常见的这个压缩格式比对,一个都对不上。这个时候虽然有点困惑,但是我又很确认它是压缩文件,因为它的刚才说了Entropy Analysis,哼,那会是怎么回事呢?这个时候就要有一个坚定的信念,就是说咱们这个可爱的独立开发者不会闲着没事自己去写一个压缩格式的,这个太扯了,不可能是Proprietary Container,绝对是某个格式,一定是往简单了猜,不要往复杂了猜。所以最终想到Deflate Stream,它可能是一个raw的Deflate Stream,没有header也没有trailer,因为它这个Magic byte不符合任何一个东西。
好,那这个猜测我觉得应该是很靠谱了,这个时候我已经很deep into the rabbit hole,我觉得应该差不多了。但是不行,还是解不开。它不是一个Deflate,这个时候就叫做一个valley of despair,我这个时候是最绝望的,觉得这个东西搞不定,什么玩意儿,怎么回事儿?还有什么可能性?直到我看到两个信息,第一个信息是说人家ES3格式经常在前面会插一个东西,有的说是插这个blob的length:后面有多长的这个文件?这个很合理——也不一定合理,因为你读一个文件读到n了,file就停了,所以说如果后面没有别的东西,那么你也不需要一个length——也可以有。还有一个思路,就是他会在这放一个checksum,那这个就更常见了,这有道理,那到这儿就猜吧。那他肯定就放一个4个byte的什么东西,别管他放了4个byte的checksum还是4个byte的length,总不会在这里放个3个byte吧?两个byte不够用,8个byte也太夸张了,又不是几个g的文件。所以我到这里只能硬猜生猜了。我丢掉4个byte,继续往前,那么回顾一下这个文件,我们正在战斗的这个binary文件,它上来有个keylength,16进制16,10进制22。然后接着它后面有个key,把这些都扔了之后,也就是一共扔了23个byte,然后再扔了4个byte后面儿的部分真的用Deflate就解开了、Python里面zlib就解开了。牛逼,好,有进展啊!那这个时候就非常非常高兴了,我以为这个事儿就已经手拿把抓了,我把我扔掉的四个byte印了一下,是一个2,800万这么一个数。这个很让人讨厌,如果这个数是大约280万,那毫无疑问就是2.8 megabyte,那这个就是我的文件后面的长度,我猜也不用猜了。那它是2,800万,那它有可能是checksum,那它是个什么checksum?有CRC,有Adler,那还有SHA256。不管它,我先把后面Deflate出来的文件一看,一个规规矩矩的JSON,什么都有。好,那我就先不管这4个byte,好吗?我先来看后面JSON吧。
我在JSON里面先做了一个非常谨慎的修改,我知道有一个东西叫做什么武学造诣,叫我的这个拳脚的造诣,这个数我现在是10,我把它10改成11。这个10改成11应该是不会触动任何改动的,然后我把它压缩回去Deflate,然后再把这个前面的byte拼回一块儿,形成了一个新的save文件,然后把它放到这个存档里面,然后去读看能不能成功。竟然失败了——也不是竟然吧,这里面同时做的事情太多了,失败也是意料之中的。他怎么个失败法呢?他在游戏里面跳出来了一个什么key verification failed,但是中文就是什么读取文件key失败,然后不等于1。诶,这个我就困惑了,他竟然还查,这可咋办呀?他如果会检查的话,那我就又得仔细看了。然后我就回来先思考,首先我不是有那个4个byte没改动吗?那这个肯定是巨大嫌疑啊。如果这是个checksum,那我的checksum当然有翻天覆地的变化。那所以我不更新checksum,他要是检查了checksum,那当然就会失败,很蛋痛。那我得先猜一猜他是什么checksum,对吧?checksum都是用这个original file来做的,但是为了保险起见,我就把Deflate压缩过的和原始的JSON——当然了,我在这个JSON要保证人家的格式没有空格,没有换行,然后这个尽量贴近它原始的格式——其实我是成功了的,我成功做到用这个Python的parser,JSON parser,但是我把它parse出去之后再compress回来它的文件cmp comp,compare之后是完全一样。也就是说我这个JSON的至少这个load和unload以及这个Deflate这一步是靠谱的。
在这样的情况下,我就想如果它是length,如果那个byte的意义其实是length的话,那么我这个应该能够读取成功,因为我的length应该没变。我把10改成11,它的byte的个数是不需要变。大概中途这个会发生的事太多了,没办法那么精确,那我先往checksum这方向猜,于是我就给我这个压缩过的以及解压缩的这个两个版本的东西都算了一个CRC checksum,这是可能性最大。可惜这个CRC checksum和原来那个byte,就是我说的2,800万,一点关系都没有。然后我看了一下,就算他不是Sign的integer,就算是Sign的integer他也是2,800万,不会变成负数。哎,这个细节我有点记不清楚了,它Sign了之后会不会变成负数?但不管怎么样,就是这个CRC checksum肯定不是。这个时候退了一步,心想如果真的是往checksum这方向去猜,这一晚上又搞不完了,checksum你必须精确的对,不对的可能性太大。而且这个2,800万这个数也让我有点起疑,我还是非常抱有巨大的希望,就是说它能不能还是一个类似于长度一样的东西?有逻辑性的东西?这个时候又是second valley of despair,就是我觉得要搞不定了。但这个时候我机智的做了一件什么事,我心想说我再拿两个存档文件看一看,那个我刚刚拿了这个一个存档啊,那我再随手再保存一个存档,拿出来看。拿出来这个存档端详,看它这个大小和之前的文件——当然大小已经有一些区别了——然后我看这看那,最后我看了keylength,我看了那个刚才说的那个checkbyte,果然有发现,机智如我,对吧?这个就是世界顶级工程师的实力。
首先我看到了他那个keylength变了,这个时候你要理解,这个说起来容易,实际上很难capture到这个点,他这么一大堆byte展现你在你面前,然后这里和那里稍微有点改变,这个就那么几个数字,几个字母,你很可能会发现不到,但我发现了。刚刚那个存档文件,它的开头的byte是16(也就是)22,现在它变成1A了,变长了还是变短了?忘了哪个是哪个了,要么就变长了,要么就变短了,反正就不一样了。噢!这个给我的线索太重要了,然后我就去看后面那个key怎么个不一样法呀?你的keylength不一样,说明你的key不一样。你key怎么个不一样了?原来这是人家存档文件的编号,我刚才不是说keyplayer,什么state,然后mm江湖这个那个,最后202310005。我不疑有他,我说0005就0005,谁知道你0005是什么东西?这可能是你内部的版本号,但是另一个存档文件里它变成20231,后面不是0005了,它为什么是一呢?因为它是1号文档,那刚才为什么是10005呢?因为它是第5号自动存档,这个用加10000在前面的方式来区分自动存档和手动存档。那我刚才在一开始也做这个文档,存档文件的时候我没有太细致去管它,我把人家的10005号存档给复制出来了,那么我之前再把它放回去的时候,我就也没记得、我不疑有他,我就把它重命名为1号存档文件,然后我去把它加载。然后这个时候游戏给我抛错误,说你这个key没有对上,不等于1。这个线索已经给的,这样一看就对上了,人家对的是这个一开头的这个明文的key,不是后面的checksum,这要对checksum,那对永远对不完了。好,那么也就是说我有两种选择,一种是我把这个存档文件重名为10005,对吧?就应该能对上。还有一个选项就是我拿人家的1号存档然后改,改完之后再重命名,放回1号手动文档位置,我选择手动文档,因为自动文存档它还会再生成,说不定还会有覆盖之类的机制,很恶心的。而且自动存档那么多,我要划几下才能找到我的10005我还要数,因为在游戏里面它又不会展示说这是10005,他只会说存档一个一个的一个列表展示。好,所以我选择在1号存档上继续我的工作,这是一个巨大的进展。
实际上我这个时候就做了一个简单的实验,就我什么都没改,我把那个10到11那个造诣都不改,直接把它解压缩,再压缩回去,然后放回那个存档位置,它当然能够正常加载。好,下一步就是我把10改成11,然后把它压缩回去,没有改我的那个现在还不知道是什么东西,但是我猜测不是checksum,我希望不是checksum,的那个4个byte。然后前面的key对应好了,存档1号,然后把它放回去——修改成功,造诣从10变成了11,这是一个巨大的进展。到了这一步我的自信就很强了,我认为这个事儿一定能解决,我一定要把它给做了。那么前面两个那个valley of despair算是过去了,实际上确实也是经历了点波折。
好,第一次成功修改了一个数了。如果我只想修改我的造诣,而且我只想从10修改成11,那我已经修改成功了。那当然不行,我得把造诣从10修改成99999,要不然修改啥呢?而且我还修改别的东西。实际上如果这个时候我光是把造诣修改成9999,它可能也不会增加它的byte,因为它取决于这个JSON具体的情况它,可能事先就预留好了4个byte让你来去放这个数值,对吧?你是0也好,1也好,也不会省一个byte,这是一个可能性;还有一个可能性是JSON确实是这么留的,但是你压缩的时候它一看是0、它一看是10,它就会把那个000给压缩掉。那你如果是一个很大的数,情况可能会有变,这个说不清楚。
好,那我就不管这么多,我来改一个别的我想改的东西,这个就到了修改的这个逻辑了,这又是一个新的大的章节,咱们就来聊修改。就是我现在能成功的修改了,但是修改能不能在游戏里符合我的需求就不一定,比如说我先随便找了一个我很想修改的东西,叫做回血速度,我希望把自己改成这个就打不死的小强,对吧?因为我选的是最高难度,然后最高难度叫江湖路难度,这个硬直啊,什么这个伤害很高,硬值很长,然后玩家没有霸体——不是没有霸体,是被打的时候就没有霸体——难度很高。我就心想我能不能把自己改成一个回血速度特别快,我打不过敌人呢,我就四处跑,然后我就自动回血了,然后再继续打,这样玩游戏很有乐趣。那我就把我的回血速度在游戏里显示了,是2——顺便说它这个回血速度和体力恢复速度,体力就是你攻击什么防御、跳跃都要消耗的体力——都在一块。那我把这个回血速度从2改成了,我先谨慎一点,改成一个24,然后我到游戏里果然不生效,然后随便看了一下这个游戏的机制,这个不生效是非常合理的,不值得奇怪:因为它这个2是加出来算出来的,它是一个最终数值,就是说虽然它把这个数值都dump了,就像我一开始说的,它这么大一个存档文件,它把整个game state都给dump出来了,但是它这个显示的2是一个derive的value,是算出来的,你把这个derive的value修改,没用。它这个2等于装备比如给你带来了1点回血速度,你的天赋给你带来1点回血速度,再来一个什么debuff又降低了1点恢复速度,总之最后凑成一个2。那你把这个2改成24,那人家问了,你这个24是从什么加过来的呀?这个不work。那我就回到我的存档文件。你看我这个时候已经能顺滑的这个来回修改了,我又进入了这个修改游戏逻辑的这个过程,就非常相对来说非常简单,迭代很快,而且又非常有自信。
那我又找到一个区域,这个区域它的名字叫什么?start raw attribute,那听起来就像是初始数值,对吧?那我就把我这个人物的初始的回血速度能不能提高很高,我就改了一下,结果发现还是不行,还是不影响我的回血速度,这个里面我就有不同的猜测:有可能它这个——因为像这种初始什么什么数值,像是一个硬的值,如果是一个硬的数值,就是一个constant,一个常量的话,它虽然存档时候也存了,但它其实是当然是没必要存的,因为不会改。不会改的话他也不会从存档里面读,对吧?当他要问说好了我要算这个人物的回血速度,我需要用他的别的回血速度再加上他的初始值,人家会从它的那个代码里面这个常量来读,而不是会从你这个破存档来读,这个存档改成什么都没用,然后等到它再存档的时候就会再dump,再dump的时候就又覆盖了。
好,那这个还不成功,但是我就想修改回血速度,这都到这一步了,能有什么难的呢?肯定能修改成功,于是我就往下翻,然后我发现有一个很可爱的改法,就是我有这个装备,它这个里的装备很暗黑破坏什么——就是随机词条。随机词条再外加随机数值范围。那有一个我的什么挂饰,就给我增加1%的吸血,再外加2的回血速度。我就把这个挂饰给改了,改成了给我加24的回血速度,然后更高的吸血比例,原来不是1%吗,现在改成5%、10%的。然后我就回去看:很神奇,回血速度还是没有改成功,它不生效,还是增加回血速度2。我给它改成什么数字它都不读的,然后看了一下它这个装备词条,当然还有一点复杂度,它有所谓的什么什么attribute zhu,attribute fu。因为我们懂拼音,肯定知道就是主任和副主任的那个主和副,那谁知道它里头是怎么改的?但是我的吸血改成功了,改成了50%。好,那这个感情好,那我就继续改吧,我给它改成了600%,1,000%,这样我打一下别人我就能吸好多血,唉,这样玩起来也挺有意思,我就不用到处乱跑了,我就和他打命。
好,我改了,然后这个时候读取存档失败了,他跳的错误还是什么key这个东西。哎,这个时候虽然这个问题看起来不太好解决,但是我已经不需要慌了,因为你就想吧,我能改成5%,能改成10%,却不能改为100%,改成1,000%,两大可能:第一可能是他这些字段他都会修,他都会验证的,它就不允许你这个值这么高,你这个值太高了。还有一个可能是,你这个改动会影响它的byte的个数,所以它那个前面那个我们的mysterious 4 bites,就我们一直没有修改的那个数就不对了。所以这个地方验证失败两大可能:第一个可能是游戏逻辑的问题,不太好验证,那我怎么来验证这个事儿呢?哎,非常精妙,学着点——我去找一个值,这个值肯定可以改的很大,比如说你的金钱,没有听说过你的金钱不能涨到几千几万,那我现在的金钱才是什么100文钱,什么零两——它还分两种货币,挺逗的。那我就把我自己的0两改成9万两,那么这个大概率改了这个byte,但是没有改它游戏逻辑的验证。我的意思就是说你一个吸血可能吸血1,000%,它不允许,那你这个银子肯定允许,我有9万两银子。那在这种情况下,如果我把这个修改放进去,他还给我失败,那就不是因为游戏逻辑——我刚才猜测的可能一,那一定是可能二了,就这么定。然后我把那个吸血先改回来,然后把银子改过去,然后我把这个存档文件存好放过去,发现它果然读取失败。OK,完全符合我的大概率的推测,它就是因为这个文件产生了变化,不是游戏逻辑问题。
好,这个时候我们就不得不来面对我们这个mysterious byte。因为你看前面是key,我的key已经对了,后面是个Deflate,这个Deflate必然对,因为如果不对的话,它是解压缩不出来的,我能把它解压缩成JSON,还能把它再压缩回去,说明我后面是对的。那这个中间只有这4个byte不对,也就是这个2,800万它是个什么东西啊?我这个后面的文件长度大概是2.8 megabytes,它怎么出一个2,800万呢?是刚好差8倍吗?我算了一下,不是刚好差10倍吗?反正也不是,也有点误差。稀奇古怪,我真是猜不出来。
那这个时候我还是想了比较久的,因为它是一个byte的东西,我一开始没有想到说我去做这种很直接的manipulation,我就没有想到我能把这个做对,而且你想一个byte,它有Big Endian,有Small Endian,就大家都知道,还有就是Signed和Unsigned 2×2=4 种可能性,也就是说这一个byte我能解读出4个不同的数。那我当时如果取Unsigned,而且取应该是Small Endian,我会解读出2,800万,那如果我取别的方式的话,就会解读出别的数,Big Endian它会变成3个billion这个数,这个我当时一看就知道不太可能。这个Unsigned Big Endian,然后Unsigned Little Endian,就是2,800万,就是我认为这个其实最接近于正确的数。然后还有两个可能性,刚才没覆盖的是这个Signed,如果是Signed的话,Little Endian还是2,800万,这个也比较符合我的预期,就它就是一个正数,所以Two's Complement也不会把它变成负数。如果是Signed的Big Endian就会变成负的9亿多。总之Big Endian本来就用的少,这个基本上默认是Little Endian,所以我就基本上锁定它就是个2,800万,然后呢?这个2,800万到底是什么意义?我真的想不出来,然后我就又去翻他那个easy safe破烂Specs。然后我发现他说你这个文件可以放你的uncompressed original file的length,可是我那个original JSON file是41MB,也不是28MB。如果是28mMB,那我就乐开花了,我就解决问题了。
Confuse, very confused. 想来想去,后来我意识到一件什么事儿,你看我都到这一步了,我离真相会非常近。我在每次猜测的时候都去以一个工程师的素养去做最大可能性的猜测,作为一个工程师,不要像数学家、逻辑学家一样去死抠那个很小可能性的严谨性。你一定要用大概率去猜,比如说我知道我们亲爱的开发者,他不会加密,于是我就按着不加密猜,果然猜对了;我估计他们不会去做checksum,那猜对了;我估计他们一定会用主流压缩格式,不会去自己搞没听说过的压缩格式,也猜对了;包括他们在dump那个整个game state的时候,我看到一个数修改了不生效,那我的猜测肯定是说因为它读的常量,我不会猜测一些奇怪的东西,什么人家用了Obfuscation,人家用了反作弊,人家用了什么这个把所有的这个参数都乘以一个常量的方式来避免被修改。这个都是现实中存在的,都是反作弊的常见这个套路,但是我知道我们可爱的开发者不会在一台EA的单机游戏里面,独立开发者去搞这些事情的,然后我们每次都猜对了,所以这一次我就相信自己的判断,2,800万一定就是一个长度,一个数,是一个有逻辑的数。我不要去把它当成一个神秘的东西看。
好,那假如说它最不神秘,它就是一个长度,我别管它28MB和41MB为什么对不上的问题,我就去把它加1怎么样?因为你看我做了什么修改,我把我的银两数从0两银子修改成了随便9,000两银子。这个大概率就是会让我的byte增加吧?既然byte增加了,这是一个长度,我别管它是什么长度,加1会如何呢?我就先试了一次,好,失败了。关键点区分这个前5%工程师和前1%工程师的这个地方又来了:就是你有技巧、你有判断、你有经验,但有的时候你还要有这个强大的决心、精神力、意志力,你就知道我肯定是对的——我是这么好的一个工程师,我怎么能猜错呢?这个时候还会有一批人放弃,我怎么会放弃?我是超人。我把那个byte又加了1——byte加2。好了,加载了,我有9,000两银子了,所以果不其然这就是一个长度,然后在看到这个结果的时候发了一会呆,我就心想这是什么东西?怎么什么个情况啊?以后我修改的时候到底是应该往上面加几个byte?然后我就又去搜了搜,我发现C#里面去Serialize一个JSON,可能和Python、js或者什么的实现还真不一样,你按说应该一样,但是区别可能出现在哪呢?哇,太有意思了。
这个存档文件里有大量的中文,怎么encode这个中文?怎么用这个JSON?怎么用bytes来encode这个中文?就不同的做法,有用UTF-8,同样是用UTF-8,也有是把它那个做成那个Escape Character,\u,用这种方式来存,就会把一个中文字存成长长的这个\u什么0005。反过来如果用UTF-8存就会更紧凑一点。也许还会有个别的一些神经病的环境用UTF-16来存,God knows。但是把这个区别考虑上之后,因为有大量的中文,所以确实有可能。我这里看到41MB是因为它是用一个更浪费一点儿的方式存的,而C#里面默认给我来了一个更骚的,长度更短的方式去存,这个听起来非常C#,就是越是这种非脚本语言,他越觉得自己要注重性能,设计的可好了,他会给你整一些幺蛾子,让它的可读性降低,然后可能节省空间。
脚本语言我们讲究就是简单粗暴、短平快,所以说就会文件大一点,基本所有的谜底都揭开了,没有一个bug,没有一个点是没想明白,没有解决的。那么接下来我去回到我的那个小小的装备饰品上——因为大家当然记得我的目的不是改出9,000两银子,你在江湖路里面就算有了9,000两银子,你照样要被地痞流氓暴打。我是要把自己改成一个回血超快的,这个不死鸟,或者是把自己改成一个能够一拳就吸血的一个这个特种战士。于是我回到我刚才报错的那个地方,我当时不是把我的吸血从5%、10%改成吸血1,000%,失败了吗?这次我把它随便改成了吸血1,000%。然后在byte数量上面,我先试了加一个byte还是报错,然后又试了加两个byte——因为它这个1,000%在这个存档文件里非常有意思,是用1万来存的,也就是说它最小单位是0.1%,他用这个整数来存这个一般会取两位,他取一位是稍微有点格的,最常见的就是百分比都用整数存,然后取两位,然后货币都用整数存,然后是按分来算。好,那么最后我就成功了。
成功了之后,我在游戏里就暴打地痞,这个地痞打我两拳最高难度,我要没血了,我回他一拳,我又满血了,非常满意。好吧,这就是这一次的debug过程,记得还是比较清楚的,我感觉也有很久没有做这样的工作了。这可能也是我别说人生当中,never say never,二十几年当中可能很少数的一次或者最后一次,要深入的来看一个技术细节。我希望应该是如此,就是如果天天做这个,也确实太花时间了。
那另一方面,我现在做的这个工作暂时AI还做不了,我是非常确定的,但我也知道很快,我不知道有多快,AI就能把这一切都做了。AI把我的这一切都能做,而且做得更快更好之前我还有多少时间呢?我们还有多少时间呢?那么在AI能够不光把这件事做了,而且他还会煞有介事的录一段自己的声音,去把这个事情记录下来,写一个博客,写一篇日志,那距离这一天的到来我们又还有多少时间呢?一般大家会认为这个更久一点,距离他产生感情、自主意识,距离他开始去记录自己作为AI的存在,我们普遍认为这还会有一段时间,但是谁又知晓,也许也很快了。那么想到这里,我就觉得这一切是很有意义的,这样的记住是非常有意义的。
这也会是最后的几篇我的debug日志,在这之后AI会能够帮我做所有的这些工作,把我的存档文件修改好,做好debug。AI有一天还会产出这样的可以说是文艺作品,或者至少是这种有价值的work,lack of a better word真的没有更好的词来描述了,这个这样的一篇记录没有什么文艺价值,所以叫他文艺作品实在是很奇怪、很勉强。但是有一天AI就会开始产出这样的内容,直到那一天到来之前,我们的最后几篇记录一定是有意义的,就是这样。
No comment
00:00
/ 50:30