欢迎光临散文网 会员登陆 & 注册

pvzclass是如何实现的?pvzclass源代码初步分析(9)Extensions.h & utils.h

2021-08-16 09:02 作者:__W1thoutD0ubt  | 我要投稿

Extensions.h 和 utils.h 都可以实现一些常用的修改功能。

Extensions.h 大部分修改都是功能类的,而 utils.h 的修改都是数值类的。

不同于之前介绍的头文件,这两个头文件直接定义了函数,而非先声明,再由其他文件完成定义。

建议在阅读本篇前先阅读第4篇的内容。

注:本文以2021.8.12更新的版本为准。

Extensions.h 

Extensions.h 开头有这样一段宏定义:

前三个宏比较简单,是一个带控制的 WriteMemory() ,适配三种整数型变量。‘

剩下的五个宏学过汇编的一眼就能看出来,这是把汇编语言翻译成机器码。

Extensions.h 实现的部分功能仅仅在跳转条件上进行调整,另外定义一个无参数版本的宏也可以方便修改。

Extensions.h 的大部分功能都是这样的(有的还需要其他参数):

以 OverlapPlanting(b) 为例

可以看出,之前 MEMMOD 类宏中的"b"就是函数的参数。显然它就是个开关。

而图示函数在 MEMMOD 类宏中直接使用数值,从写入的地址来看,这应该是机器码。

当然,作者显然没有把机器码直接背下来,而是利用其他软件,将汇编代码转化为机器码。

对于一段汇编代码,作者大部分采取转化为机器码后拼接的方法。因此,Extensions.h 中会出现这种乍一看很反常的代码:

看地址,像是修改代码;看数值,像是修改常量

其实大部分时候这还是在修改代码,只是形式上有些偷懒而已。

当然,也有使用其他方法实现自身功能的函数。比如 VasePerspect(b),就是用常规方法进行代码注入:

虽然形式变了,但基本的实现方式并未变化。

utils.h

和 events 组件一样,utils.h 也是由 YouTheB(github名)补充到 pvzclass 中的。

实际上这个文件并未包含在 pvzclass.h 中。

utils.h 中所有函数都包含在命名空间 Utils 中。

utils.h 和大部分头文件一样,开头都是 #pragma once 和包含的头文件。

然后是变量类型定义和宏定义:

果然,pvzclass 的绝大部分功能都有这俩。

但在文件末尾,两个宏定义被移除了:

前六个是修改植物基本属性的函数,原理都很简单。

在PVZ中,这些基本属性是用结构体数组(当然也可能是二维数组)存储的。

地址变量 address 的第一部分是统一的 0x69F2C? ,实际上就是指定了某一属性(或第一维)。第二部分就是下标(或第二维)了。

SetSunValue(sun) 更简单,单纯 Write(x,y) 而已。

readmemory(t) 则是个套皮 Read(t) 。

真·套皮

剩余的三个函数中,isvalid(s,l,r) 和 t(a) 都是 GetAddress(s,l,r) 的辅助函数。

不难看出,isvalid(s,l,r) 判断一个字符串(或其子串)是否满足中括号匹配

而 t(a) 则是套皮的 isalnum(a) 。

isalnum(a) 可以检查某一字符是否为数字或英文字母(包括大小写),如果是就返回 true ,否则返回 false。

下面分析 GetAddress(s,l,r)。

GetAddress()

在汇编语言中,常用 [address] 表示指针 address 所指向的值。

但这种表示形式不能嵌套,对于多级指针,只能用多个语句实现。

而 GetAddress(s,l,r) 可以直接识别这种表达式(但不能有减法运算)并给出结果,即使表达式中有多层嵌套。

我们从头开始分析:

第一部分

这段代码的作用很明显,是在一开始判断表达式是否有括号失配的低级错误。

时间复杂度%5Crm%20O(n)

第二部分

这里 top 只有定义而没有应用,它在下一段代码才能派上用场。

这一部分中 l 和 r 的作用应该很明确了。它们表示的是子串的范围。

这一部分的作用为:判定 s(或它的一个子串)是否表示 16 进制数值。如果是,就将其转为 10 进制并返回。

这一部分的时间复杂度也是%5Crm%20O(n)

第三部分

从这部分代码开始,s 肯定是一个中括号括起的表达式。

开头出现了一个名为 v 的 vector<int>。

结合开头将 l-1、中间将 r 分别加入 v,以及每当有同级 + 就将编号加入 v,可以初步判定,这是在根据 + 将字符串分割为若干子串。

这一段的时间复杂度为%5Crm%20O(n)

又因为这个表达式默认只有 + 一种二元运算符,这些子串要么是 16 进制数,要么是中括号括起的表达式。

接下来的代码也不难理解,就是递归处理各个子串。

如果是 16 进制数,就会在自调用的第二部分代码算出数值,并立刻返回;如果是表达式,就会继续递归,直到求得数值为止。

在运算部分的代码中,need 表示这个子串是否由中括号扩起。结合中括号的作用和代码内容,不难理解该变量的意义。

GetAddress() 前有这样一句注释:


这个%5Crm%20O(n)大概率指的是时间复杂度。

实际上,对于部分字符串,如括号失配的字符串、纯16进制数串,时间复杂度确实是%5Crm%20O(n),原因显然。

不过对于另一部分字符串,实际上时间复杂度就有可能大于%5Crm%20O(n)

在第三部分代码中,每次递归都只会去除一层中括号,剩下的子串会在递归调用第三部分的代码时重复遍历,拉低计算效率。

对于形如"[[[[[[[[[[[[......6A9EC0]]]]]]]]]]]]......."(左右中括号数量相等,6A9EC0可以换为其他 16 进制数),该算法的时间复杂度甚至会达到%5Crm%20O(n%5E2),而且还有大量开 vector 导致栈溢出的可能。
不过一般情况下%5Crm%20n不会很大,而且%5Crm%20O(n%5E2)的情况也不容易出现,还是可以接受的。

下一篇将分析events,可能是pvzclass(时间上)最多变的部分。

pvzclass是如何实现的?pvzclass源代码初步分析(9)Extensions.h & utils.h的评论 (共 条)

分享到微博请遵守国家法律