2025-2-10 19:19
漫漫苦短
一、回數与天气
回数其实就是回合的意思,我为了忠实于英杰传的源代码而这么命名,那么这两者这间有什么关联,并且与其他内容如何联系,这节先简要研究一下。
这两者最大的关系是它们在游戏中的内存空间是在一起的,而且都和DS:CF6A这个地址有关,下面看以下英杰传代码,反汇编代码都是可以写注释,每条语句后面加上; 就是它的注释,也可以放在两条语句中间。
[code] ; si在此之前已经=CF6A
seg002:2D16 C6 44 02 00 mov byte ptr [si+2], 0 ;現在回數设为0
seg002:2D1A C6 44 04 01 mov byte ptr [si+4], 1 ;第0回數天气设为1[/code]
[code] ; di在此之前已经=CF6A
seg002:3D26 8A 45 03 mov al, [di+3] ;限制回數
seg002:3D29 38 45 02 cmp [di+2], al ;比较現在回數与限制回數
seg002:3D2C 72 03 jb short loc_30C51 ; [di+2]<[di+3] 跳转
seg002:3D2E E9 C9 00 jmp loc_30D1A
seg002:3D31 loc_30C51: ; CODE XREF: sub_30C28+24↑j
seg002:3D31 57 push di
seg002:3D32 9A 62 3E F2 2C call sub_30D82 ;更新天气值
seg002:3DFA loc_30D1A: ; CODE XREF: sub_30C28+26↑j
seg002:3DFA 1E push ds
seg002:3DFB 68 28 30 push offset asc_437F8 ; "戰鬥拖長了,所以要撤退.\n"
[/code]
通过这两段我们可以看到,ds:[CF6A+2]保存的是現在回數,ds:[CF6A+3]保存的是限制回數,ds:[CF6A+4]保存的是天气值,每当回數+1都会更新一次天气值这与我们在游戏中看到的是一样的。
[code]seg002:3D72 57 push di
seg002:3D73 9A D8 3F F2 2C call sub_30EF8 ;天气
seg002:3D78 8A D8 mov bl, al
seg002:3D7A 2A FF sub bh, bh
seg002:3D7C 03 DB add bx, bx
seg002:3D7E FF B7 14 2D push word ptr [bx+2D14h] ; 0"晴", 1"雲", 2"雨"
seg002:3D82 8A 45 02 mov al, [di+2] ;現在回數
seg002:3D85 2A E4 sub ah, ah
seg002:3D87 40 inc ax
seg002:3D88 50 push ax
seg002:3D89 1E push ds
seg002:3D8A 68 42 30 push offset asc_43812 ; "第%w回合 %s"
seg002:3D8D 6A 00 push 0
seg002:3D8F 9A 42 32 F6 1C call sub_201A2
seg002:3D94 83 C4 0A add sp, 0Ah
dseg:2D0A B4 B8 00 asc_434DA text "Big5", '晴',0 ; DATA XREF: dseg:2D14↓o
dseg:2D0D B6 B3 00 asc_434DD text "Big5", '雲',0 ; DATA XREF: dseg:2D16↓o
dseg:2D10 AB 42 00 asc_434E0 text "Big5", '雨',0 ; DATA XREF: dseg:2D18↓o
dseg:2D14 0A 2D dw offset asc_434DA ; "晴"
dseg:2D16 0D 2D dw offset asc_434DD ; "雲"
dseg:2D18 10 2D dw offset asc_434E0 ; "雨"[/code]
这就是我们在每个回合开始的时候看到的本回合信息,%w会被替换为(現在回數+1),而%s会被替换为"晴", "雲", "雨",根据bx来确定。
[bx+2D14h]可能看着既熟悉又陌生,这其实是汇编语句中的动态获取内存地址的值的方法。
当sub_30EF8函数返回的是0时,获取的是DS:[0+2D14], 也就是2D0A,指向"晴"这个字符串,返回的是2时,获取的是DS:[2+2+2D14]=DS:[2D18]
[code] ;这个函数arg_0值只有CF6A
seg002:35E6 8B 5E 06 mov bx, [bp+arg_0]
seg002:35E9 2A E4 sub ah, ah
seg002:35EB 8A 47 03 mov al, [bx+3] ;限制回數
seg002:35EE 50 push ax
seg002:35EF 8A 47 02 mov al, [bx+2] ;現在回數
seg002:35F2 40 inc ax
seg002:35F3 50 push ax
seg002:35F4 1E push ds
seg002:35F5 68 6D 2F push offset asc_4373D ; "現在回數%2w/%2w"
seg002:35F8 9A 32 32 F6 1C call sub_20192
seg002:35FD 83 C4 08 add sp, 8
seg002:3600 EB 15 jmp short loc_30537
seg002:3602 loc_30522: ; CODE XREF: sub_303DA+12A↑j
seg002:3602 8B 5E 06 mov bx, [bp+arg_0]
seg002:3605 2A E4 sub ah, ah
seg002:3607 8A 47 03 mov al, [bx+3] ;限制回數
seg002:360A 50 push ax
seg002:360B 1E push ds
seg002:360C 68 7E 2F push offset asc_4374E ; "限制回數%2w"
seg002:360F 9A 32 32 F6 1C call sub_20192
seg002:3614 83 C4 06 add sp, 6
seg002:3617 loc_30537: ; CODE XREF: sub_303DA+146↑j[/code]
通过上面的分析,这段是什么意思应该不难看出吧。
接下来就是如何更新天气的部分了,龙吟前辈已经给出了其中很详细的更新过程了,这部分就当是朝花夕拾了,想当初我刚开始研究找到这部分的代码,为了把main.exe的内容与之对应上,就费了一会功夫,希望今后其他人的研究就别这么吃力了。
[code]seg002:3E62 sub_30D82 proc far ; CODE XREF: sub_30C28+2A↑P
seg002:3E62
seg002:3E62 var_2 = byte ptr -2
seg002:3E62 var_1 = byte ptr -1
seg002:3E62 arg_0 = word ptr 6 ;这个函数arg_0值只有CF6A
seg002:3E62
seg002:3E62 C8 02 00 00 enter 2, 0
seg002:3E66 56 push si ;在栈中保存si原来的值
seg002:3E67 8B 76 06 mov si, [bp+arg_0]
seg002:3E6A B8 06 00 mov ax, 6
seg002:3E6D 9A E0 3D F6 1C call sub_20D40 ;产生天气随机数
seg002:3E72 88 46 FE mov [bp+var_2], al
seg002:3E75 8A 44 04 mov al, [si+4] ;获取上回合天气代码
seg002:3E78 88 46 FF mov [bp+var_1], al
seg002:3E7B 3A 46 FE cmp al, [bp+var_2] ;比较上回合天气代码与天气随机数
seg002:3E7E 76 05 jbe short loc_30DA5 ;
seg002:3E80 FE 4E FF dec [bp+var_1] ;上回合天气代码大于随机数,更新为代码-1
seg002:3E83 EB 0B jmp short loc_30DB0
seg002:3E85 loc_30DA5: ; CODE XREF: sub_30D82+1C↑j
seg002:3E85 8A 46 FE mov al, [bp+var_2]
seg002:3E88 38 46 FF cmp [bp+var_1], al ;比较上回合天气代码与天气随机数
seg002:3E8B 73 03 jnb short loc_30DB0
seg002:3E8D FE 46 FF inc [bp+var_1] ;上回合天气代码小于随机数,更新为代码+1
seg002:3E90 loc_30DB0: ; CODE XREF: sub_30D82+21↑j sub_30D82+29↑j
seg002:3E90 80 7E FF 00 cmp [bp+var_1], 0 ;比较更新后的天气代码与0
seg002:3E94 75 06 jnz short loc_30DBC
seg002:3E96 C6 44 04 05 mov byte ptr [si+4], 5 ;相等,天气代码由0改为5
seg002:3E9A EB 12 jmp short loc_30DCE
seg002:3E9C loc_30DBC: ; CODE XREF: sub_30D82+32↑j
seg002:3E9C 80 7E FF 05 cmp [bp+var_1], 5 ;比较更新后的天气代码与5
seg002:3EA0 75 06 jnz short loc_30DC8
seg002:3EA2 C6 44 04 00 mov byte ptr [si+4], 0 ;相等,天气代码由5改为0
seg002:3EA6 EB 06 jmp short loc_30DCE
seg002:3EA8 loc_30DC8: ; CODE XREF: sub_30D82+3E↑j
seg002:3EA8 8A 46 FF mov al, [bp+var_1]
seg002:3EAB 88 44 04 mov [si+4], al ;保存本回合天气代码
seg002:3EAE loc_30DCE: ; CODE XREF: sub_30D82+38↑j
seg002:3EAE ; sub_30D82+44↑j
seg002:3EAE 5E pop si ;还原si原来的值
seg002:3EAF C9 leave
seg002:3EB0 CA 02 00 retf 2
seg002:3EB0 sub_30D82 endp[/code]
似乎还漏了一点,为什么0, 1, 2代表0"晴",3代表1"雲",4, 5代表2"雨",看了这部分就不会迷惑了吧。
[code]seg002:3FD8 sub_30EF8 proc far
seg002:3FD8
seg002:3FD8 arg_0 = word ptr 6 ;这个函数传入的参数值只有CF6A
seg002:3FD8
seg002:3FD8 55 push bp
seg002:3FD9 8B EC mov bp, sp
seg002:3FDB 8B 5E 06 mov bx, [bp+arg_0]
seg002:3FDE 8A 5F 04 mov bl, [bx+4] ;获取天气代码
seg002:3FE1 2A FF sub bh, bh
seg002:3FE3 8A 87 4E 2E mov al, [bx+2E4Eh]
seg002:3FE7 C9 leave
seg002:3FE8 CA 02 00 retf 2
seg002:3FE8 sub_30EF8 endp
dseg:2E4E 00 00 00 01 02 02 db 0, 0, 0, 1, 2, 2[/code]
[color=Silver][[i] 本帖最后由 漫漫苦短 于 2025-2-10 19:44 编辑 [/i]][/color]
2025-2-15 19:19
漫漫苦短
二、地图代码和宽高、关卡状态
上节介绍了天气和回合都是与CF6A有关,但是只占用了DS段的[CF6A+2][CF6A+3][CF6A+4],也就是还有[CF6A][CF6A+1],那么它们又代表什么?
ds:CF6A占1字节,它代表的是地图代码,每一张地图都有唯一代码,注意徐州I和徐州II用的是同一幅地图,夏丘彭城新野襄阳雒天荡山的I和II都是如此,但葭萌关瓦口关宛许昌邺这些关虽然名字相同,但用的不是同一幅地图,地图代码是不一样的,用过龙吟的剧情编辑和地图编辑工具应该对此有所了解,有时间总结一下所有的地图代码。
[code]seg002:3EB4 sub_30DD4 proc far ; CODE XREF: sub_2FB7A+F↑P
seg002:3EB4
seg002:3EB4 var_C = byte ptr -0Ch
seg002:3EB4 var_4 = byte ptr -4
seg002:3EB4 arg_0 = word ptr 6 ;arg_0=CF6A
seg002:3EB4
seg002:3EB4 C8 0C 00 00 enter 0Ch, 0
seg002:3EB8 8D 46 F4 lea ax, [bp+var_C]
seg002:3EBB 50 push ax
seg002:3EBC 68 5E C2 push 0C25Eh
seg002:3EBF 9A 1E C5 F6 1C call sub_2947E
seg002:3EC4 8B 5E 06 mov bx, [bp+arg_0] ;BX=CF6A
seg002:3EC7 8A 46 FC mov al, [bp+var_4]
seg002:3ECA 88 07 mov [bx], al ;保存地图代码
seg002:3ECC C9 leave
seg002:3ECD CA 02 00 retf 2
seg002:3ECD sub_30DD4 endp[/code]
这里先不分析sub_2947E的具体作用,但我们要注意的var_4并没有在seg002:3EB8-seg002:3EC4之前有类似mov [bp-var_4], al的代码,但却把var_4赋值给了al,但这其实不是程序有误使用未初始化的数据传值,因为有些函数需要的返回值不只1个可以直接保存在AX中,这时就有两个解决方法:
[list=1]
[*]在传入函数参数的时候,先传入一个地址值,比如说lea ax, [bp+var_C]这句,就是获取var_C的地址的值保存在AX中,以指针的形式传给sub_2947E函数,这样sub_2947E函数可以以它的arg_0(bp+var_C的地址)为首地址修改其偏移的地址的值。
[*]把返回值传入以一个地址为首开始的一段连续内存空间,并且返回该地址给AX。
[/list]
2的方法在代码中的应用碰到了再介绍,2的方法识别起来更加容易,因为在函数的末尾通常会出现把将一个固定地址传给AX,而且在前文会对这个固定地址以及其偏移地址进行修改。
读取了地图代码后就可以进行一系列的操作了,比如读取这个地图的宽和高,保存在[CF68]和[CF69],CF68,CF69都比CF6A小,但没有用[CF6A-2]和[CF6A-1]的方式而是使用byte_4D738和byte_4D739这样独立的方式进行读取。
这是两种方式的读取,而采用哪种方式并没有太大的差别,这与程序的编译有关,编译中如果有的数据独立出来更好生成汇编代码就有可能采用独立的方式读取。
[code]seg002:2A35 68 1A 55 push 551Ah
seg002:2A38 9A 08 1D F6 1C call sub_1EC68
seg002:2A3D 5B pop bx
seg002:2A3E 8B D8 mov bx, ax
seg002:2A40 8E C2 mov es, dx
seg002:2A42 26 8A 07 mov al, es:[bx] ;从ES{551A}:0读取地貌图的宽
seg002:2A45 D0 E8 shr al, 1 ;除以2得出地形图的宽W
seg002:2A47 A2 68 CF mov byte_4D738, al ;保存到DS:CF68
seg002:2A4A 68 1A 55 push 551Ah
seg002:2A4D 9A 08 1D F6 1C call sub_1EC68
seg002:2A52 5B pop bx
seg002:2A53 8B D8 mov bx, ax
seg002:2A55 8E C2 mov es, dx
seg002:2A57 26 8A 47 01 mov al, es:[bx+1] ;从ES{551A}:1读取地貌图的高
seg002:2A5B D0 E8 shr al, 1 ;除以2得出地形图的高H
seg002:2A5D A2 69 CF mov byte_4D739, al ;保存到DS:CF69[/code]
sub_1EC68函数在番外1就有简介了,提到申请内存的时候还会有详细说明。
注意seg002:2A45和seg002:2A5B的shr al, 1这句,这就是移位运算,al向右移1位,也就是al向下整除2。英杰传中有相当多的整除运算不保留小数,只能保留余数。shl是左移运算,相当于乘法。
整除2的原因是英杰传同一幅地图有两种宽和高,一个是常见的地形图,就是每个坐标代表着一种地形;另一种是地貌图,每一个坐标的地形由四块地貌组成,每一种地貌也对应着代码,这样就构成了形形色色的战场画面,显而易见地貌图的宽高都是地形图的两倍,总的大小是地形图的四倍。更多的说明可以参见龙吟的[url=http://www.xycq.org.cn/forum/viewthread.php?tid=244530]三国志英杰传战场地图文件分析[/url]。
[code]seg002:2C68 56 push si ;SI=CF6A
seg002:2C69 9A B4 3E F2 2C call sub_30DD4
seg002:2C6E 32 C0 xor al, al
seg002:2C70 A2 08 2D mov byte_434D8, al ;DS:2D08
seg002:2C73 A2 09 2D mov byte_434D9, al ;DS:2D09
seg002:2C76 88 44 05 mov [si+5], al
seg002:2C79 88 44 01 mov [si+1], al[/code]
这段代码中的[si+5]和[si+1]就是表示关卡状态,其中[CF6A+5]比较简单,就是0一个状态1一个状态,就是在不操作的时候,部队也会变换动作,就是不停在0和1之间切换,但只是把[CF6A+5]改变并不能直接实现动画效果,这个相当于只是一个标志。
[code]seg002:6B2A sub_33A4A proc far
seg002:6B2A
seg002:6B2A arg_0 = word ptr 6 ;arg_0=CF6A
seg002:6B2A
seg002:6B2A 55 push bp
seg002:6B2B 8B EC mov bp, sp
seg002:6B2D 8B 5E 06 mov bx, [bp+arg_0]
seg002:6B30 8A 47 05 mov al, [bx+5]
seg002:6B33 C9 leave
seg002:6B34 CA 02 00 retf 2
seg002:6B34 sub_33A4A endp[/code]
[code]seg002:6B38 sub_33A58 proc far
seg002:6B38
seg002:6B38 arg_0 = word ptr 6 ;arg_0=CF6A
seg002:6B38
seg002:6B38 55 push bp
seg002:6B39 8B EC mov bp, sp
seg002:6B3B 8B 5E 06 mov bx, [bp+arg_0]
seg002:6B3E 80 77 05 01 xor byte ptr [bx+5], 1 ;1与0异或(xor)为1,所以[bx+5]会在0和1之间不停切换
seg002:6B42 C9 leave
seg002:6B43 CA 02 00 retf 2
seg002:6B43 sub_33A58 endp[/code]
[CF6A+1]是整个关卡的状态代码,00=未结束 01=全军撤退 02=胜利(殘存部隊) 03=失败,具体是如何变换的就先不进行分析了。
[code]seg002:4138 sub_31058 proc far
seg002:4138
seg002:4138 arg_0 = word ptr 6 ;arg_0=CF6A
seg002:4138
seg002:4138 55 push bp
seg002:4139 8B EC mov bp, sp
seg002:413B 8B 5E 06 mov bx, [bp+arg_0]
seg002:413E 8A 47 01 mov al, [bx+1]
seg002:4141 C9 leave
seg002:4142 CA 02 00 retf 2
seg002:4142 sub_31058 endp[/code]
2025-2-18 19:19
漫漫苦短
三、关卡名称和过关经验值(50点)
其实除了上面介绍CF6A的一些组成部分,包括了DS:CF6A-DS:CF6F(CF6A+5)的地址空间,在CF6A+6和后面有N字节部分,保存着现在进行的关卡名称。
[code]seg002:2CBD 56 push si ;si=CF6A
seg002:2CBE 9A 04 3F F2 2C call sub_30E24 ; 保存关卡名称到DS:CF70中
...
seg002:2CF8 6A 10 push 10h
seg002:2CFA 6A 20 push 20h
seg002:2CFC 68 A0 01 push 1A0h
seg002:2CFF 68 60 01 push 160h
seg002:2D02 68 90 CF push 0CF90h
seg002:2D05 9A CE 45 F2 2C call sub_314EE[/code]
这里发现调用了两个函数,上面的那个函数就是保存关卡名字,下面的就是在战役开始之前初始化整个节目,其中就包括在最上方输出的那个关卡名称。
[code]seg002:3F04 sub_30E24 proc far ; CODE XREF: sub_2FB7A+64↑P
seg002:3FC4 83 C1 06 add cx, 6 ;cx=CF6A
seg002:3FC7 1E push ds
seg002:3FC8 51 push cx ;此处是关卡名称字符串的首地址DS:CF70
seg002:3FC9 1E push ds
seg002:3FCA 68 81 30 push offset asc_43851 ; "之戰"
seg002:3FCD 9A B8 3C F6 1C call sub_20C18 ;字符串连接,关卡名称后面加上之"之戰"
seg002:3FD5 sub_30E24 endp[/code]
这个字符串连接(Concat)函数可以说是一个不起眼但在后面的分析中会经常用到的,大部分的程序中保存字符串都是占用一段连续的空间,最后以00(不是数字0)作为结尾,而要想将两个不同地址的字符串拼接在一起,就要用到这样的函数,比如说一个字符串占用DS:OFF1的地址长度为a,占用空间DS:OFF1-DS:(OFF1+a-1),另一个字符串占用DS:OFF2的地址长度为b,两者合并在一起就会占用空间DS:OFF1-DS:(OFF1+a+b-1),这样的作用就是可以一起输出两个字符串,而不用考虑分别放在什么位置。
[code]seg002:45CE sub_314EE proc far ; CODE XREF: sub_2FB7A+AB↑P
seg002:46D7 68 6A CF push 0CF6Ah
seg002:46DA 9A 2A 41 F2 2C call sub_3104A ;获取关卡名称字符串的首地址
seg002:46DF 50 push ax
seg002:46E0 68 23 31 push offset asc_438F3 ; "%s"
seg002:46E3 68 26 51 push 5126h
seg002:46E6 9A 3E 0C F6 1C call sub_1DB9E ; 显示战役名字
seg002:46EB 83 C4 06 add sp, 6
seg002:4790 sub_314EE endp[/code]
[code]seg002:412A sub_3104A proc far
seg002:412A
seg002:412A arg_0 = word ptr 6 ;arg_0=CF6A
seg002:412A
seg002:412A 55 push bp
seg002:412B 8B EC mov bp, sp
seg002:412D 8B 46 06 mov ax, [bp+arg_0]
seg002:4130 05 06 00 add ax, 6
seg002:4133 C9 leave
seg002:4134 CA 02 00 retf 2
seg002:4134 _sub_3104A endp[/code]
从DS:[CF6A+6]到DS:[CF6A+23]中都是关卡名称的内存空间,有种想法是不同关卡的名字长度都不同,那为啥不动态调整它们所占内存的长度,而是要固定其中的长度,可能有以下原因,一是在静态语言编写的程序中,都是要向内存申请固定长度的空间,因为这样方便程序的管理,二是动态调整的话有可能会影响到其他数据块的地址,固定长度更加方便。
DS:[CF6A+24]就是新的数据内容了,它就是每关的过关经验值。
[code]seg002:3BE6 sub_30B06 proc far ; CODE XREF: sub_30C28+14C↓P
seg002:3BE6
seg002:3BE6 var_4 = word ptr -4
seg002:3BE6 var_1 = byte ptr -1
seg002:3BE6 arg_0 = word ptr 6 ;arg_0=CF6A
seg002:3BE6
seg002:3BE6 C8 04 00 00 enter 4, 0
seg002:3BEA 57 push di
seg002:3BEB 56 push si
seg002:3BEC 8B 7E 06 mov di, [bp+arg_0]
seg002:3BEF 80 7D 01 02 cmp byte ptr [di+1], 2 ;这个就是前面介绍的关卡状态
seg002:3BF3 75 77 jnz short loc_30B8C
seg002:3BF5 80 7D 24 00 cmp byte ptr [di+24h], 0 ;判断是否有过关经验值,0表示没有
seg002:3BF9 74 14 jz short loc_30B2F
seg002:3BFB 8A 45 24 mov al, [di+24h]
seg002:3BFE 2A E4 sub ah, ah
seg002:3C00 50 push ax
seg002:3C01 1E push ds
seg002:3C02 68 09 30 push offset asc_437D9 ;"殘存部隊得到\x1BC6%w\x1BC7點經驗值."
seg002:3C05 6A 01 push 1
seg002:3C07 9A 42 32 F6 1C call sub_201A2 ;弹出获得经验值的提示框
seg002:3C0C 83 C4 08 add sp, 8[/code]
关于这个过关经验值是怎么设置的,以及过关经验值是如何加在每只部队上的,这里就先不细述了。
总结,关于CF6A的数据块,以及其中的成员就先概述这么多了,总之如果把这个数据块想象成一个家族,那么这个家族只有一个老大,就是CF6A这个地址,要想访问其中的成员,要通过CF6A来操作,而且其中的成员不多,关系也不复杂,接下来要介绍的可能就要复杂多了。
[color=Silver][[i] 本帖最后由 漫漫苦短 于 2025-2-18 19:52 编辑 [/i]][/color]