pvzclass是如何实现的?pvzclass源代码初步分析(9)Extensions.h & utils.h
Extensions.h 和 utils.h 都可以实现一些常用的修改功能。
Extensions.h 大部分修改都是功能类的,而 utils.h 的修改都是数值类的。
不同于之前介绍的头文件,这两个头文件直接定义了函数,而非先声明,再由其他文件完成定义。
建议在阅读本篇前先阅读第4篇的内容。
注:本文以2021.8.12更新的版本为准。


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

前三个宏比较简单,是一个带控制的 WriteMemory() ,适配三种整数型变量。‘
剩下的五个宏学过汇编的一眼就能看出来,这是把汇编语言翻译成机器码。
Extensions.h 实现的部分功能仅仅在跳转条件上进行调整,另外定义一个无参数版本的宏也可以方便修改。
Extensions.h 的大部分功能都是这样的(有的还需要其他参数):

可以看出,之前 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) 可以直接识别这种表达式(但不能有减法运算)并给出结果,即使表达式中有多层嵌套。
我们从头开始分析:

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

这里 top 只有定义而没有应用,它在下一段代码才能派上用场。
这一部分中 l 和 r 的作用应该很明确了。它们表示的是子串的范围。
这一部分的作用为:判定 s(或它的一个子串)是否表示 16 进制数值。如果是,就将其转为 10 进制并返回。
这一部分的时间复杂度也是。

从这部分代码开始,s 肯定是一个中括号括起的表达式。
开头出现了一个名为 v 的 vector<int>。
结合开头将 l-1、中间将 r 分别加入 v,以及每当有同级 + 就将编号加入 v,可以初步判定,这是在根据 + 将字符串分割为若干子串。
这一段的时间复杂度为。
又因为这个表达式默认只有 + 一种二元运算符,这些子串要么是 16 进制数,要么是中括号括起的表达式。
接下来的代码也不难理解,就是递归处理各个子串。
如果是 16 进制数,就会在自调用的第二部分代码算出数值,并立刻返回;如果是表达式,就会继续递归,直到求得数值为止。
在运算部分的代码中,need 表示这个子串是否由中括号扩起。结合中括号的作用和代码内容,不难理解该变量的意义。

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

这个大概率指的是时间复杂度。
实际上,对于部分字符串,如括号失配的字符串、纯16进制数串,时间复杂度确实是,原因显然。
不过对于另一部分字符串,实际上时间复杂度就有可能大于。
在第三部分代码中,每次递归都只会去除一层中括号,剩下的子串会在递归调用第三部分的代码时重复遍历,拉低计算效率。
对于形如"[[[[[[[[[[[[......6A9EC0]]]]]]]]]]]]......."(左右中括号数量相等,6A9EC0可以换为其他 16 进制数),该算法的时间复杂度甚至会达到,而且还有大量开 vector 导致栈溢出的可能。
不过一般情况下不会很大,而且
的情况也不容易出现,还是可以接受的。

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