【摸鱼】rUGP简单分析



楼主 展鸿丶  发布于 2020-06-18 13:52:00 +0800 CST  
事情是这样的……



楼主 展鸿丶  发布于 2020-06-18 13:53:00 +0800 CST  
一、ICI文件


游戏的主要资源文件就是这几个了,其中rio和rio.002根据文件名可推测是分卷形式,仍需进一步分析。
经过简单的CreateFile断点之后,发现rugp.exe先读取了ici文件,所以先对ici文件进行分析。
经过简单跟踪,发现主要的代码都在UnivUI.dll里,加载了mfc120等符号之后,可以发现文件读写使用了MFC的CFile类。
继续跟踪之后发现引擎给CFile保证了一层,类名是CSizedFile。




先创建了一个CSizedFile然后调用虚函数Open实际打开了文件。
如果打开成功,会进入CreatePmArchive这个函数。
这个函数是一个工具函数,用于创建CPmArchive对象。
上面代码简化之后就是这样:
CFile* pFile = new CSizedFile();
pFile->Open( pszFileName );
ar = CreatePmArchive( pFile );
return ar;


进入CPmArchive的构造函数之后发现CPmArchive里有一个CArchive对象。


这个CArchive是MFC的东西,所以需要查看MFC的源码。网上以及MSDN都有详细介绍。
简单说CArchive就是MFC为了方便APP存储和载入文档而造的一个轮子,相当于一个文件读写工具。
说高端点就是对象の序列化と反序列化。
从这个函数出来,CArchive/CPmArchive对象已经构造完整,并且可以开始读写文件了。
接着跟踪几步之后,就会到LoadForChild这个函数,当然名字是我随便写的。


由于Ici文件是个加密文件,所以并不走这个分支,而是接着往下走。



先通过CPmArchive::HeapSerialize从文件中读取并解密数据。加密数据中包含了Heap的大小,所以不需要读取时指定。
把数据读取到Heap之后,还要经过一次解密(UTIL_DecryptIci),才能得到明文的Ici数据。


得到明文数据之后,就开始进行读取载入了。

楼主 展鸿丶  发布于 2020-06-18 14:23:00 +0800 CST  
二、OceanNode
看到上面的代码之后,估计应该都很想知道这个COceanNode是什么东西吧。
这个OceanNode贯穿整个rUGP引擎,是这个引擎的Framework。
这个Node其实就是一棵树,就是数据结构里的那种树。
OceanNode把整个rUGP引擎里所有的对象以及资源组织成了一棵树。
如果有学过U3D或者一些现代游戏引擎,应该对这个概念比较熟悉。


可以看出这是一个典型的树形结构,指向父级的指针,包含子级的容器。
先一个个来。
上面说到,OceanNode用于组织引擎中的各种对象,所以每个OceanNode当然有一个指向对象实例的指针。
即:m_pObj
然后就是用于存储子节点的容器:m_pMap
它指向一个CNodeMap对象,定义如下:


这是一个HashMap的简单实现。
它通过计算节点的名称(即m_strName)的Hash值来确定要把节点指针放在哪个桶里。
由于Hash值可能会冲突,所以每个桶里的节点指针通过链表形式来存储该桶里的其它节点,这就是m_pNext的作用。
光是文字可能不够形象,所以我弄张简单的结构图。


整个引擎绝大多数对象都会通过OceanNode组织起来。
(包括所有的游戏素材(这个才是重点吧(划掉)))
每个节点(的对象)还可以接收消息做出相应动作,这个后面再说。

楼主 展鸿丶  发布于 2020-06-18 14:41:00 +0800 CST  
三、序列化和反序列化。
即:把内存中的对象实例存储到文件中、从文件中读取数据并重新构建对象实例,的过程。
该引擎通过MFC的CArchvie来实现该过程,所以当然要学习CArchvie的用法才行,而CArchvie、CObject、CRuntimeClass是MFC的关键三要素,所以必须简单地学习一下MFC才行。
当然网上有各种资料,而且MFC也是开源的,这里我并不想过多赘述,我只做简单的说明。
每个需要序列化/反序列化的对象都必须继承CObject,因为这样才有相关的功能函数来实现它。
CObject有一个虚函数:Serialize 只要实现这个函数,对象就可以支持序列化。
想把一个对象存储到文件中时,只需要简单调用:ar << obj; 即可,其内部会调用Serialize来实现具体的数据读写。
对象的序列化当然还涉及反射,也就是CRuntimeClass,当需要从文件中读取一个对象时,会先读取该对象是什么类,然后创建该类的实例,最后调用Serialize从文件中读取该对象的数据,完成反序列化。


以上是简单例子,详细的关于MFC的对象序列化可以在网上搜索到相关资料。

楼主 展鸿丶  发布于 2020-06-18 14:56:00 +0800 CST  
四、OceanNode的序列化
初步了解序列化相关知识之后,现在进入LoadForChild_1这个函数。




先解释一个关键的参数“pChild”
如果传进来NULL,将会创建一个新的子节点,然后从文件中载入该子节点的数据。


函数进来之后,通常会先走CPmArchive::ReadRootClass来读取第一层信息。


里面还套了一层,继续跟进。


首先读取了一个DWORD(即4字节整数)作为MagicNumber,判断类型。


往下读取了版本号和Flags,接着就会进入CPmArchive::ReadClass


这个函数基本就是照抄CArchive,增加了一个读取加密RuntimeClass的功能。
该函数从文件中读取一个类名(字符串),例如“CObjectOcean”,然后从程序中查找该类是否存在,如果存在则返回相应的CRuntimeClass指针。
然而实际上这里是CRioRTC指针,该类继承了MFC原版的CRuntimeClass,增加了一些数据。这里不多解释,后面才有用。
下文RuntimeClass简称RTC。
读完RTC之后就返回到COceanNode::LoadForChild_1了。


先是创建相应的子节点和该子节点管理的对象(Object)


然后会走到这个switch,COceanNode::LoadChildren如其名,即是载入该节点的子节点。
这里暂且略过。
载入完子节点之后,会回到LABEL_38。这里调用了该节点所管理的对象的序列化函数,即从文件中读取数据并重新构建该对象。


至此一个OceanNode就载入完成了。
下面再看看COceanNode::LoadChildren

楼主 展鸿丶  发布于 2020-06-18 15:17:00 +0800 CST  
五、OceanNode子节点的加载


COceanNode::LoadChildren先从文件中读取了一个整数,即该节点下的子节点的数量。


然后计次循环,读取所有子节点。
根据相关Flag判断应该创建子节点还是通过名称来查找现有的子节点进行更新。
创建好子节点后,就会调用 COceanNode::Deserialize 或者 COceanNode::Serialize 来执行该子节点的反序列化过程。
这两个函数实现差不多,只不过用途不同。


进入 COceanNode::Serialize 函数


首先是读取Flag以及ReadCTypeClass。
其中ReadCTypeClass还细分为:
CPmArchive::ReadClass
CPmArchive::ReadMsgClass
CPmArchive::ReadBasicTypeClass
总之就是返回一个RTC指针。


接着读取了两个了DWORD,分别是该节点管理的对象的序列化数据在文件中的偏移量和大小。
然后把该节点加入全局的OceanNode Map中,而Key即是偏移量。
该Map在引擎中起到至关重要的作用,后面所有需要引用某一个子节点的时候都通过Key来直接查找相应的节点。


以上即完成了该节点的读取,但还没完。因为该节点也可以有子节点。
所以递归执行 COceanNode::LoadChildren




递归完成出来回到这里之后,该节点的所有子节点就读取完成了。


你可能注意到上面的代码只读取了节点的少数数据,例如RTC(即m_pClassRef)以及Addr、Size
是的,COceanNode::LoadChildren 只载入了这些,并不会同时加载每个子节点所管理的对象。

楼主 展鸿丶  发布于 2020-06-18 15:36:00 +0800 CST  
六、对象的加载
即OceanNode所管理的对象(m_pObj)
想要加载该对象,则需要知道该对象的序列化数据存在哪里。
该数据即是OceanNode的:
m_dwResAddr和m_dwResSize
这两个数值是加密的,需要经过简单的解密运算才能得到真正的数值。


至于为什么叫Addr呢,这跟rio文件的分卷有关,我第一时间联想到的是地址空间的概念,所以就随意起了这个名字,反正它就是个offset。
注意:实际上 DecryptedOffset 还需要经过一次运算才能得到真正的偏移量,后面会说到。


rio文件分卷:
引擎把所有rio文件分卷连接成一个地址空间
假设分卷①文件的大小是2GB,则该分卷的地址空间就是 0字节~2147483648字节
分卷②的地址空间则从 2147483649字节开始,以此类推。
OceanNode中存储的ResAddr的值范围一定会落在所有分卷拼成的完整地址空间中。
所以读取数据时,还要根据Addr来区分数据具体在哪一个分卷里。确定了分卷之后,就要通过减法来计算真正的文件偏移了。
有了以上基础,就能从所有分卷中读取指定Addr处的对象数据。
当然引擎肯定有一个函数用来加载对象,即这个:






介绍完对象的加载之后,终于可以回到ici加载了。

楼主 展鸿丶  发布于 2020-06-18 15:57:00 +0800 CST  
七、CObjectArcMan
实际上ici文件里的数据就是CObjectArcMan对象的数据。
通过加载ici文件,可以构建出一个CObjectArcMan对象,该对象包含了非常重要的信息。
首先它存储了游戏的基本信息,以及安装信息,这些数据会在安装游戏时用到。
它包含每个rio分卷文件的信息,通过一个CInstallSource对象数组来存储。
CInstallSource存储了一个分卷文件的所有信息,例如:该分卷的名称、地址空间范围、校检信息等等。
除了rio分卷信息,CObjectArcMan中还包含了游戏的入口点:CrelicUnitedGameProject对象的Addr。
游戏中所有的东西都包含在CrelicUnitedGameProject这个对象中。


只有读取了CObjectArcMan后,才能知道怎么读取rio分卷文件。

楼主 展鸿丶  发布于 2020-06-18 16:09:00 +0800 CST  
八、CrelicUnitedGameProject
这个对象是游戏里的顶级对象了,如其名,它组织了一个游戏中所有需要用到的素材。
当然,就如前面所说,所有的对象都由OceanNode进行组织管理。
为了方便理解层级关系,我做一个简单的图。


CStaticOceanRoot就是整个引擎中OceanNode的根节点。至于它的name为什么不叫Root of the Ocean……(不是)
它的子节点里有:
通过COceanNode::LoadForChild来加载的CObjectArcMan节点。
以及依然是通过COceanNode::LoadForChild来加载的CrelicUnitedGameProject节点。
当然还有其它的节点,不是很重要所以忽略掉。

楼主 展鸿丶  发布于 2020-06-18 16:23:00 +0800 CST  
九、素材
想要提取那当然是要遍历CrelicUnitedGameProject节点的所有子节点,找到想要的东西。
但不知道从哪个版本开始,rUGP把素材“藏起来”了,也就是说,仅通过LoadForChild载入CrelicUnitedGameProject是无法找到想要的东西的,那还有什么办法吗?
当然有,想要在游戏里显示一张CG那肯定是有相应的指令的。
而这些指令存储在rUGP的脚本对象CRsa里。
而这些CRsa是可以直接遍历CrelicUnitedGameProject找到的。
所以只要加载每一个CRsa对象,并分析其中的指令,就能得到几乎所有素材的信息,拿到Key(即offset)之后就可以去找分卷文件来读取素材对象了。

楼主 展鸿丶  发布于 2020-06-18 16:29:00 +0800 CST  
十、CRsa
通过ResAddr和ResSize读到CRsa对象的序列化数据之后,就可以进行反序列化了。
如图:
CRsa::Serialize函数


前面有几个整数信息,其中一个加密标记,如果有加密,则需要调用CPmArchive::HeapSerialize来读取并解密。
HeapSerialize就是上面读ici用到的那个,只不过key换了。


很显然CRsa::Serialize只是把一堆数据读进内存,并没有做更多解析。所以还需要找其它函数。
网上一翻就找到了CRsa::ExtractData这个函数。
它负责解析刚才读到Heap里的一堆数据。


前面是几个整数,包含了版本号,命令对象池的大小(字节),命令数量,等等。


分配命令对象池的内存。


循环读取并创建所有命令对象实例。


可以看出来,一个CRsa对象其实就是一堆命令(CVmCommand)的集合。
并且它们是按顺序排列的,这是为了方便存档和读档。
存档时只需要存储当前执行到了哪一条命令即可。(大概吧(不是))


在CRsa中,基本的命令有这些:


其中有些命令通过名称就可以大概猜测其功能。
例如:
CVmCall 调用其它命令或CRsa脚本
CVmFlag 对Flag进行操作(例如设置某条文本是否已读)
CVmGenericMsg 执行引擎基本命令,其中包括几十种不同的命令,大多数名字以“OM_”开头,可以调用引擎中各种功能。
CVmImage 还没有进行分析,猜测用于加载或显示图片。
CVmJump 跳转到指定的CVmLabel处执行
CVmLabel 用作定位标记
CVmMsg 用于显示游戏中人物对话的文本
CVmRet 结束CRsa脚本,使脚本执行完毕
CVmSound 播放声音
CVmSync 未知

楼主 展鸿丶  发布于 2020-06-18 16:45:00 +0800 CST  
十一、提取游戏文本
简单说就是,分析CVmMsg::Serialize即可。


打了个简单的log可以看看:


楼主 展鸿丶  发布于 2020-06-18 16:48:00 +0800 CST  
十二、CVmGenericMsg
这个命令用于发送一个CRioMsg给指定的OceanNode


消息类型定义如下:



消息参数定义。


m_pParamArray会指向一个静态数组,其中包含多个CMsgParam。


消息实体定义如下:




上面有提到,COceanNode里的m_pClassRef指向 CRioRTC 对象实例。
所以这里给一下定义:






上面似乎忘了提到 CRio 这个东西。
引擎中几乎所有能存储到文件中的对象都继承于CRio。
比方说CrelicUnitedGameProject的继承关系是这样的:


最底层还是MFC的CObject


回到本题。当想要给引擎中某一个OceanNode发送一个消息时,首先要创建一个该消息的实例。
简单来说就是这样:


然后调用:
SendRioMsg
COceanNode::SendRioMsgNoArgs
COceanNode::SendRioMsgToAllChildren
或者其它未知的函数就可以把一个RioMsg发送给指定的OceanNode


SendRioMsg 首先会根据目标OceanNode的m_pClassRef(CRioRTC)来找到m_pRioMsgArray,即 Msg Handler Array,如果在该数组中找到了能于MsgRtc匹配的Handler,就调用该Handler,处理该Msg。
如果找不到对应的Hander,则 SendRioMsg 什么也不干。






引擎各种动作都依赖RioMsg,例如:




关于RioMsg就暂且到这里,有这些基础,在分析其它的东西,会清晰很多。

楼主 展鸿丶  发布于 2020-06-18 17:16:00 +0800 CST  

楼主:展鸿丶

字数:6231

发表时间:2020-06-18 21:52:00 +0800 CST

更新时间:2021-03-22 21:53:34 +0800 CST

评论数:31条评论

帖子来源:百度贴吧  访问原帖

 

热门帖子

随机列表

大家在看