文章深入详细分析大航海时代online对话框信息内存数据。
这里所说的对话框,包括一切在游戏内弹出的不可交互式和可交互式窗口,包括人物信息、技能列表、船只信息、交易所买卖界面、睿智之书,甚至账号密码输入框、退出游戏确定提示,都统称为对话框。
先上一张成果图:
CE找核心对话框代码
首先,已知台服v12010版本的对话框基址是121E544。打开CE,添加地址121E544。
从游戏逻辑来说,在游戏内做打开对话框动作,游戏是先生成对话框内容,再读取对话框基址显示对话框内容。反应到CE上,就是在没有对话框的时候,对话框基址是000000,打开对话框后,对话框基址有数值。生成对话框的过程很复杂,所以我们跟踪读取对话框过程。因此在121E544上右键选“什么访问了这个地址”。
然后在游戏里反复打开、关闭对话框,仔细观察CE窗口。通过观察,发现D24B27和D24B3C这两个地址,只有在打开对话框的时候计数会变。而这两个地址是一个代码段的。因此从D24B27入手,继续跟踪。
X64DBG跟踪
x64dbg载入游戏并登录游戏,直接跳转到D24B27代码段,在代码段开头下断。然后游戏内按F1快捷键打开人物信息对话框。x64dbg成功断下来,F8单步到D24B27行:
此时[121E544]地址数值是0,ecx=[121E544],再判断ecx是否为0,跳到了D24B38,读esp+4的值到eax,再[121E544]=eax,之后直接ret了。所以这个函数就是确定 [121E544] 的值,再到上一级行数里继续跟踪。
F8返回到D21D0C,如下图:
下面有3个CALL,怎么确定是哪一个?现在程序已经有了[121E544]的数值,接下来的CALL要用到这个数值。而C语言一般是ecx或堆栈来传递值。所以继续F8单步,观察下面CALL时的ecx和堆栈。并且要循环对话框,在汇编里就是有一个向上跳转的标志。
经过观察,00D00D21D48 call <gvo12010.sub_D25D10>这个CALL非常像,ecx=[121E544],且有2个向上跳转。
由于已知在CE里可以读出来鼠标位置对话框的地址[121E544+C],再反过来跟踪验证D25D10,可以确定这就是遍历对话框地址函数。
经过研究,程序逻辑是:
- 通过ecx传入[121E544]的值
- D25D10行,[ecx+1C]是ecx的子对话框的基址。判断子对话框数值是否为0。不为0继续。
- D25D39行,[esi+24]=1继续,=0跳到D25D5A
- D25D46行,CALL EBX作用未知,结果eax=1
- D25D4C行,判断[esi+1c],=1表示当前层级的当前对话框还有子对话框,再CALL D25D30通过递归实现继续分解。=0没有子对话框,跳到D25D5A。这句之后,ecx=子对话框基址,esi=当前对话框基址。
- D25D5A行,[ESI+14]是当前层级的当前对话框的下一个对话框基址。这句相当于循环里的变量迭代。
- D25D61行,判断当前对话框是否是最后一个,不是则跳到D25D43继续循环
python遍历代码
仿照程序逻辑和汇编代码,我写了一个python遍历对话框代码。实测可用,但你没办法直接复制使用,你需要把gvo.read_addr改成读取航海内存的代码。或者根据思路自己写个按键精灵的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | def enum_dialog(_addr, _list=None) -> list: """ 枚举当前打开的对话框信息 Parameters ---------- _addr: int 传入的对话框基址 _list: list 返回的列表。由于可能多次嵌套调用,需要用参数保存返回值 Returns ------- Returns: list 枚举的列表 ------- """ if _list is None: _list = [] _addr = gvo.read_addr(_addr + 0x1C) while _addr: temp_dict = {} if gvo.read_addr(_addr + 0x24) == 1: # 读取当前对话框基础信息 temp_dict['addr'] = hex(_addr) temp_dict['id'] = hex(gvo.read_addr(_addr + 0x4)) temp_dict['pos_x'] = gvo.read_addr(_addr + 0x34) temp_dict['pos_y'] = gvo.read_addr(_addr + 0x38) temp_dict['width'] = gvo.read_addr(_addr + 0x4C) temp_dict['height'] = gvo.read_addr(_addr + 0x50) _list.append(temp_dict) # [_addr + 0x1C] 是否有当前对话框的子对话框数据,=0没数据,=1有数据 subaddr = gvo.read_addr(_addr + 0x1C) if subaddr: temp_list = enum_dialog(subaddr, _list) _list.append(temp_list) # [_addr + 0x14] 是否有当前对话框的同级下一个对话框数据,=0没数据,=1有数据 _addr = gvo.read_addr(_addr + 0x14) return _list dialog_base = 0x121E544 print("base_addr = " + hex(dialog_base).upper()) a = enum_dialog(dialog_base) print(a) |
实际利用数据
id
代码效果如下图,每一个对话框都有addr、id、坐标及其他数据。比较重要的是id。id中2ee?开头的都是按钮。也可以根据ID判断脚本是否打开了正确的对话框。
坐标
坐标是相对于对话框的坐标。比如下图id=2ee0的坐标,是返回按钮左上角,相对于船只信息对话框左上角的坐标值(650,380),按钮宽56,高24。在游戏分辨率较大或者缩放了DPI时,可根据对话框相对坐标调整鼠标点击位置。
数值
可以根据子对话框基址+偏移获取所需数据。比如载货界面,船只的货仓状态。
载货界面右下角货仓数据,也是一个小的对话框。地址是06633E08,物资=06633E08+8C,交易品=06633E08+90,仓位=06633E08+94
文章评论
通过这个从船的窗口进去,就能找到当前耐久上限了吧?之前一直没找到这个
@Keo 船的耐久有个单独的静态地址吧?你从船只信息里找,当对话框不打开的时候,看不到耐久的。
这篇文章能否再补充点怎么找到打开窗口call,以及找到窗口基址的思路
@Keo 窗口CALL就麻烦了,我也在学习。找窗口基址的思路是有个内存地址,窗口关闭=0,打开=1。从这里逆推出来基址
感谢提供思路,最近刚好在研究大航海,希望博主多写点这些内容,让我们小白多学习学习
大航海时代 Online 版本 12.030
找出了对话方块的基底位置为 0x01720EE4,也成功地读取出人物情报跟载货的交易品资讯
可是在读取持有物品一览时,目前只找到了几个资讯
持有物品数量 ( 偏移值 658 ) / 持有物品数量最大值 ( 偏移值 614 )
船长袋收藏数 ( 偏移值 6F4) / 船长袋收藏数最大值 ( 偏移值 61C )
档案夹收藏数 ( 偏移值 6D8) / 档案夹收藏数最大值 ( 偏移值 618 )
接着要读取物品一览的资讯时,使用了物品的 ID、属性内容 ( 耐久度 )等已知讯息下去搜索,怎么找都找不出相符合的记忆体纪录。
最后用了消耗物品的数量以未知初始值跟增减的方式下去找,发现记忆体实际内容最为可能的纪录是,消耗物品数量x 256 ( 1 = 0x0100,200 = 0xC800 ),也买了好几种消耗用品进行研究,发现+0x14 是下一种消耗用品的数量,但进去该位置浏览记忆体时,发现数量跟我研究出来的相符合,可是又找不到消耗物品的ID。
请问要如何找出物品一览的详细资讯,还是有什么文章或者思路可以参考的,感恩!
另外请问在周围人物算法分析中,是要如何确定基底位置?
@笨蛋一枚 不知道你有没有学过c语言的数组?持有物品可以看做一个数组。当然实际上,他更像是c++语言的STL标准模板库的map容器,或者python语言的字典。键(key)就是物品ID,值(value)是游戏自定义的结构体。所以,你需要根据物品ID来找,不能用耐久度找。物品的说明、耐久度这些都是存储在其他地方的数据,不在持有物品里。
你想要找物品的详细资讯,就需要先理解游戏的对话框,学会分析持有物品子对话框,然后学会分析持有物品数组,最后分析物品值(value)的游戏自定义结构体。
@笨蛋一枚 周围人物算法找基址,最快捷方便的方法是你去一个偏僻的NPC房间,数一数房间内NPC加上你自己角色,一共有几个人,比如是3个人。用CE搜索数值3。然后控制另外一个角色,进入这个房间,CE搜索4。控制另一个角色离开房间,CE搜索3。反复搜索几次,就可以搜到一个静态地址。这个地址前面4字节就是基址。
昨天是在商店买陆战用品,今天搜索别的消耗物品,数量又不对了,头大。
感恩!谢谢指导,我再研究看看。