植物大战僵尸吧 关注:554,940贴子:5,041,997

关于出怪列表的机制及其衍生研究

只看楼主收藏回复

用Windows Phone版(后简称WP版)和原版反汇编对照着看的,主要WP有公开的C#的源码,阅读起来比汇编方便多了)。为保证严谨我把原版的汇编也对照着读了一遍,机制上感觉上没什么太大差异,但WP版多一些像快速游戏之类的模式,还有几个从来没见过的小游戏,细节处理有些差异,总体上的处理逻辑还是一样的。
注:下文中未特殊说明外,变量名全部来源于C#的源码中(毕竟源程序编译为目标程序的过程中,变量名全部被地址所取代,反汇编看不到变量名),内存地址都是原版中的。
文中可能会出现一些C代码和汇编代码,但即使看不明白的话理解结论应该还是没问题的(((
推荐阅读:p/8229511229(read/cv21358315) (《关于出怪列表的填充:级别容量上限、固定出怪……》)
本文前半部分可看作对此帖子的补充,后半部分中其实有些前人也写过(但很零散),本文算是做补充和修正,以及记录自己的一些新发现。


IP属地:上海1楼2024-08-15 21:46回复
    0. 前置知识
    出怪列表在内存中的位置:[[[0x6a9ec0]+0x768]+0x6b4]
    (除特殊注明外,本文所有寻址符号“[]”均以四字节来解析,下同)
    从此地址开始的2000个数据,每个数据为四字节的整型,存放的是僵尸的编号(0~32),每50个数据为一组作为一波的出怪,共可容纳40波。每波的结束以-1为结尾,(满了50只最后就不用填-1了)观察内存时可能会发现-1后面有之前其它模式的僵尸数据残留,这不会被游戏计入不要紧。在打开无尽后再打开冒险模式的10w关卡,11~20w的部分也会有数据残留,也不会被游戏计入。


    IP属地:上海2楼2024-08-15 21:47
    收起回复
      0.5.再探级别上限公式
      之前广为流传的如下所示:
      一类公式:每波容量上限 = int(int((当前波数) * 0.67~0.68) / 2) + 1
      二类公式:每波容量上限 = int(int((当前波数) * 0.8) / 2) + 1。
      看着这个0.67~0.68有些好奇,想知道游戏里到底写成什么样,感觉像是2/3,所以特地翻了一下汇编代码,结果发现汇编代码写得更离谱:

      (EBX为当前波数,ECX为计算得到的级别上限)
      翻译一下:(记当前波数为W,从0起算;为方便理解,我把移位运算都写成除以2的次方的形式)
      第一行是二类公式,第二行是一类公式

      (这一写法其实不严谨,这里暂且把W当无符号整数处理,不然公式会写得更复杂,因为有符号数除法默认向0取整)


      IP属地:上海3楼2024-08-15 21:49
      回复
        看到这段代码大家应该都很摸不着头脑,可能在想宝开在弄什么名堂。其实刚看到的时候我也很摸不着头脑,后来才查到是编译器对除法指令的优化导致的(因为除法运算的效率很低)。
        这帖子毕竟不是讲汇编语言的,具体的优化机制我不多说,只讲以下两条结论:
        (看不明白也没关系)
        (1)除以3的操作会被转换为乘0x55555556然后右移位;
        (2)除以5的操作会被转换为乘0x66666667然后右移位。
        所以事实上推测游戏内部写的公式是:(记当前波数为W,从0起算)
        二类公式:int(2w/5)+1
        一类公式:int(w/3)+1
        比之前的结论应该稍微简洁一些。至于旗帜波公式比较复杂,见后面章节。
        其实从数学的角度来说,是不可能仅通过乘法和除以2的次方来实现除以3、除以5的操作的,所以这些优化操作计算得到的只是一个近似值,有误差存在,但是经试验在int范围内误差小于1,不会造成任何影响。


        IP属地:上海4楼2024-08-15 21:49
        回复


          IP属地:上海5楼2024-08-15 21:51
          回复
            1.2.旗帜波普僵旗帜
            记n=该波未乘2.5前的级别上限。
            n<8,出n普僵一旗帜;
            n>=8,出8普僵一旗帜。
            此规律同时可解释1-2~1-5这些不足10w的旗帜波出普僵旗帜的规律。


            IP属地:上海6楼2024-08-15 21:52
            回复


              IP属地:上海7楼2024-08-15 21:52
              回复
                2. 再探固定出怪
                2.1. 最后一波出所有僵尸(附:两处出怪列表)
                冒险模式各关卡,在最后一波会将所有允许出怪种类先各出一个,已经在列表中的除外。
                (故函数名为PutInMissingZombies)这一操作的执行在普僵旗帜、新僵尸之后,所以不会把这些再出一遍。
                权重为0的僵尸,即使在允许出怪种类中也不会被填入列表,如小鬼。
                游戏中有两处出怪列表,一处与对应关卡绑定,一处决定当前出怪。(这个之前帖子有提到但没具体说明)
                两处列表在内存中位置及相关机制后文都会提到。
                这里“允许出怪种类”的判定来源于gZombieAllowedLevels数组,这一数组不区分一、二周目。
                该数组在原版中指针表上写的是:
                6A35B4\冒险模式1-1出怪是否有普通(+CC下一种僵尸,+4下一关)
                想修改列表还得先修改以下两个地方
                [40D6A3--jmp 40D6A8]
                [40D6AC--nop,nop]
                不难注意到0xCC/0x04=0x33=51,也就是说每51个数据作为一个整体,存放某种僵尸在冒险模式中的50关是否可以出现。


                IP属地:上海8楼2024-08-15 21:55
                回复


                  IP属地:上海10楼2024-08-15 21:57
                  回复


                    IP属地:上海11楼2024-08-15 21:58
                    回复


                      IP属地:上海12楼2024-08-15 21:59
                      回复
                        所以你会发现在用WPF修改小游戏列表时,如果僵尸种类数对应不上的话按钮是灰色的,操作无效。例如僵尸快跑,默认是普、障、杆、豚,4怪,那么函数中对应的分支就只有四个写入内存的语句,如果你选择了3种或者5种出怪类型,修改器是没法修改的,故无效。
                        Pt/PTK修改出怪时,当前的界面会立即发生变化,退出重进后出怪种类仍然原封不动,就是因为修改的是第二处列表,每次重进关卡初始化列表时,游戏内部都会重新设置这一列表的值。
                        Pt修改出怪时需要修改第二处列表(因为是调用游戏内部的逻辑),但出怪列表填充完成后Pt会自动将第二处列表复原为原来的样子,因此如果在点击“自然出怪”按钮后读内存中的第二处列表会发现根本没有变化,笔者一开始被这玩意坑了很久(笑);PTK不会复原第二处列表,点击“自然出怪”按钮后检测第二处列表会发现有改变。


                        IP属地:上海14楼2024-08-15 22:02
                        回复
                          2.2. 新僵尸
                          引用自:p/6134448446《【火星向】分析冒险6-49为什么有豌豆射手僵尸》
                          查看填充出怪列表的函数可以发现,游戏首先会寻找编号最小的首次在这一关卡出现的僵尸(找不到则返回-1),这里称其为“特殊僵尸”。
                          填充列表时,如果当前所在波为总波数的一半加一(若“特殊僵尸”为矿工或气球则为第7波)或者最后一波,就会将一只“特殊僵尸”添加入列表。问题在于,此时游戏并没有检查“特殊僵尸”是否在本关的出怪种类中。而所有植物僵尸首次出现关卡的值均为99,因此在冒险6-49,其中编号最小的豌豆射手僵尸就会刷出,且只有两只。

                          C#代码如上,那些注释是代码中本身就包含的。
                          其中“首现关”就是新僵尸首次出现的关卡,原版地址[0x69DA8C+i*0x1C],i为僵尸编号。
                          注意到红眼的首现关是48,即5-8关,但编号比白眼大所以不会被刷出。雪橇小队同理。
                          实际填充列表时规定普僵不被计入“首次出场的僵尸”,因此一周目1-1的w3、二周目1-1的w11都不会固定出普僵。


                          IP属地:上海15楼2024-08-15 22:03
                          回复
                            补充:
                            其实可以看出0x69AD8C附近的一大片内存区域是一个结构体数组,每个结构体的大小是0x1C,基于此可以把指针表扩展一下:
                            【+1C下一个僵尸】
                            0x69DA80 僵尸编号
                            0X69DA84 动画轨道类型
                            0x69DA88 出怪级别
                            0x69DA8C 首次出场的关卡
                            0x69DA90 允许最早出现的波次
                            0x69DA94 出怪权重
                            0x69DA88 指向僵尸名称字符串的指针


                            IP属地:上海16楼2024-08-15 22:03
                            回复
                              2.3. 蹦极闪电战
                              固定每个旗帜波出5只蹦极,但由于出蹦极的操作写在初始化级别上限之前,所以导致这些蹦极不占用级别上限。
                              (能看出宝开的代码写的是真乱...)
                              2.4. 固定出怪顺序
                              顺序:蹦极闪电战的旗帜波蹦极——(初始化级别上限)——普僵旗帜——(级别上限翻倍)——新僵尸——所有僵尸——柱子关固定出怪。
                              柱子关的出怪类型实际只有普、障、桶、橄。扶梯、小丑、巨人都不会在通常波次刷出,观察小游戏的函数InitZombieWaves即可。(前文有述)


                              IP属地:上海17楼2024-08-15 22:03
                              回复