[ESA]永恒的星际联盟

 找回密码
 立即注册

!connect_header_login!

!connect_header_login_tip!

查看: 439|回复: 10

EUD入门必读之基础知识

[复制链接]
发表于 2020-11-27 23:15:01 | 显示全部楼层 |阅读模式
本帖最后由 PereC 于 2020-11-29 14:16 编辑

本文初稿写于2020-11-26,之后可能会进行修改与勘误。
若想掌握eud制图技术,必须对内存表了如指掌。若想熟悉内存表,必先熟悉内存及数据储存原理。若想了解数据储存原理,就要了解二进制、十六进制。基础打牢,才可举一反三,融会贯通,学会高级技巧。基础不牢,则看啥啥不懂,毫无自学能力,只能伸手或者照猫画虎。本文将从基础知识开始,由浅入深地带你走进EUD的世界。可以说,本文是所有eud知识的基础,只有完全掌握了本文内容,才能完全看懂我写的其他eud教程。
学习本文内容所需要的工具:
(1)星际争霸重制版
(2)最新版scmd,详见https://www.scrpg.info/forum.php?mod=viewthread&tid=9859
(3)计算器、不同进制数的转换工具

参考资料:
EUD基础原理 by Roy:
http://www.staredit.net/topic/14226/
内存表(需翻墙)
http://farty1billion.dyndns.org/EUDDB/

【第一节】认识二进制数(Binary number)和十六进制数(Hexadecimal number)
人类使用十进制数(Decimal number)来记录数值的大小,十进制数的特点是:每个数由若干位组成,从小到大分别是个位、十位、百位...,每位上的数字都是0至9这10个数字中的一个。数数的时候,从0开始数,0,1,2,......,数到9的时候,下一个数要进一位,因此下一个数就是10(我们可以把它读作“一零”,方便理解)。
而计算机使用二进制数(Binary number)来记录数值的大小,特点是:每个数由若干位组成,每位上的数字只能是0或1中的一个。用二进制数来数数时,从0开始数,下一个数是1,比它大1的数是10(读作“一零”,不是“十”),比10大1的数是11(读作“一一”),比11大1的数是100(读作“一零零”,不是“一百”)。以此类推。
我们可以看到,数值是客观存在的,但是用不同进制表示出来是不一样的。比如地上摆着四个石子,我们想要用数字来表示石子的数量,那么用十进制数表示就是4,用二进制数表示就是100,虽然写出来长相不一样,但是它们所表示的数值都是“四”这个数值。
由于二进制数写起来太麻烦,所以我们通常使用十六进制数(Hexadecimal number)来表示计算机内储存的二进制数据。十六进制数的特点:由若干位组成,每位都是0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F这十六个数中的一个,数数时:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F,20......
同理,n进制数的特点也不言而喻:由若干位组成,每位上的数字有n种,并且逢n就进一位。

p01

p01


日常生活中,我们写的所有数都会被默认为是十进制数,比如你写了100,人们就会把它理解为“一百”这个数值。但是当我们在计算机领域内讨论问题时,你写一个100,别人不知道这个数是什么进制,它可以被理解为十进制,表示的数值为“一百”,也可以被理解为二进制,表示的数值为“四”,甚至可以被理解为八进制等等。所以,当无法从前后文推断出一个数是什么进制时,为了消除歧义,我们约定:当一个数是二进制数时,我们在它前面加上"0b"这个记号。当它是十六进制数时,我们在前面加上"0x"这个记号。当一个数是十进制数时,我们不用添加任何多余的记号。
例:
12345: 表示十进制数12345,所代表的数值是12345
1000: 表示十进制数1000,所代表的数值是1000,即一千
0b1000: 表示二进制数1000,所代表的数值是8
0b00001000: 同上,表示二进制数1000,所代表的数值是8。高位填充的0可以无视
0b00010001: 也可以写成0b10001,表示二进制数10001,所代表的数值为17
0x10: 表示十六进制数10,所代表的数值是16
0x000000FF: 表示十六进制数FF,所代表的数值是255

一个数值无论以何种进制来表示,都会是一串数字,从左到右由高位写到低位,其中最右侧的数字叫做最低位,最左侧的叫做最高位。比如246这个数,我们不用管它是什么进制,数字"6"所在的位置叫最低位,数字"2"所在的位置叫做最高位,数字"4"所在的位置相对于"6"是高位,相对于"2"所在的位置是低位。
每一位上的数字都有它自己的意义。当246是十进制数时,"6"代表6个1,"4"代表4个10,而"2"代表2个100,即十进制数从最低位到最高位的数字的基数分别为1, 10, 100, 1000......
所以:
十进制的246 = 2*100 + 4*10 + 6 = 2*10^2 + 4*10^1 + 6*10^0
十进制的62039 = 6*10^4 + 2*10^3 + 0*10^2 + 3*10^1 + 9*10^0
其中a^b代表a的b次方。
同理,二进制数从最低位到高位的数字的基数分别为二进制的1, 10, 100, 1000..., 即2^0, 2^1, 2^2, 2^3...,即1, 2, 4, 8...
十六进制数从最低位到高位的数字的基数分别为十六进制的1, 10, 100, 1000..., 即16^0, 16^1, 16^2, 16^3...,即1, 16, 256, 4096...
n进制数从最低位到高位的数字的基数分别为n^0, n^1, n^2, n^3...
即:
十六进制的246 = 2*16^2 + 4*16^1 + 6*16^0
十六进制的62039 = 6*16^4 + 2*16^3 + 0*16^2 + 3*16^1 + 9*16^0
二进制的110010 = 1*2^5 + 1*2^4 + 0*2^3 + 0*2^2 + 1*2^1 + 0*2^0
n进制的vwxyz = v*n^4 + w*n^3 + x*n^2 + y*n^1 + z*n^0

所以,我们很容易得到将二进制数和十六进制数转为十进制数的方法,即用这个数的每一位数乘以它的基数,然后把它们加起来:
0b110010 = 1*2^5 + 1*2^4 + 0*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 32+16+2 = 50
0b10011001 = 1*2^7 + 1*2^4 + 1*2^3 + 1^2^0 = 128+16+8+1 = 153
0xAB = 10*16^1 + 11*16^0 = 160+11 = 171
0x246 = 2*16^2 + 4*16^1 + 6*16^0 = 512+64+6 = 582

那么,如何把十进制数转化为n进制呢?使用短除法即可。将十进制数除以n,得到一个商和余数,再把这个商除以n,得到第二个商和余数,不断重复此过程,直到商为0,然后把余数从下往上依次写出来,即可得到对应的n进制数。
下面是将十进制数10 转化为二进制数的过程:

p02

p02


余数从下到上分别为1010,所以转化成的二进制数就是1010

这样,我们就得到了将十进制数转化为其他进制数的方法,和将其他进制数转化为十进制数的方法。

下面来讲二进制数和十六进制数的相互转化:
由于16正好等于2的4次方,所以二进制数和十六进制数的转化非常容易,完全不需要转化为十进制数然后再短除。
二进制数转化为十六进制数时,只需要将二进制数分为若干组即可,从最低位开始,每4位为一组,然后参照上面表格的第一列,将每组数替换为十六进制数字即可。
比如:
0b1110110101 = 0b 11 1011 0101 = 0x3B5
同理,十六进制数转化为二进制数,只需要将十六进制数的每一位上面的数字转化为其对应的二进制数即可。
比如0x70AB:
0x7 = 0b0111
0x0 = 0b0000
0xA = 0b1010
0xB = 0b1011
所以0x70AB = 0b0111000010101011

至此,二进制数、十进制数、十六进制数相互转化方法都已介绍完毕。在阅读后面的章节时,你可以使用含有此功能的计算器或网页来直接完成转化。

习题:
(1)
1011, 0b1011, 0x1011分别是多少进制的数字?分别求出他们所表示的数值(计算的结果用十进制数表示)
(2)将0xFF00和0x00FF转化为二进制数
(3)将0b111011110100101转化为十六进制数
(4)一个2位的十进制数所能表示的最大数值为多少?一个4位的十六进制数所能表示的最大数值为多少?一个8位的二进制数所能表示的最大数值为多少?


 楼主| 发表于 2020-11-27 23:17:13 | 显示全部楼层
本帖最后由 PereC 于 2020-11-30 17:13 编辑

【第二节】认识内存和内存地址
计算机储存数据使用的最小单元称为“位(bit)”,它由一些电子元件构成,并且只能处于"开"或"关"两种状态中的一种,分别记作1和0。“位”也可以简称为小写字母b。为了避免歧义,下文将用英文bit来表示这个概念。
1个bit有两种可能的状态: 0, 1
2个bit就会有4种可能的状态:00, 01, 10, 11
3个bit就会有8种可能的状态: 000, 001, 010, 011, 100, 101, 110, 111
...
计算机利用这些“位”的各种状态的组合来储存数据。
我们将8位作为一组,称为一个“字节(byte)”,也可简称为大写字母B,一个字节可以有256种不同的状态:
00000000
00000001
00000010
00000011
......
00001111
00010000
......
11111110
11111111
注意,11111110表示的是该字节的状态,也可以说它是该字节所储存的数据。可以发现,它长得很像二进制数。所以我们可以用二进制数来表示每个字节的状态。一个字节的每个状态刚好能用一个8位的二进制数与其对应。而一个8位的二进制数又刚好可以转化为一个2位的十六进制数。由于二进制数很冗长,所以我们一般使用一个2位的十六进制数来表示一个字节所储存的数据:
00000000: 0x00
00000001: 0x01
00000010: 0x02
00000011: 0x03
......
00001111: 0x0F
00010000: 0x10
......
11111110: 0xFE
11111111: 0xFF
注意!这个十六进制数只是表示了这个字节所储存的数据而已,而这个数据可以被解释为各种含义,比如可以被解释为一个数值,或者一个字符,等等等等。

在玩游戏的时候,所有的游戏数据都储存在内存中。为了更方便地存储和读取数据,内存把它的每一份储存空间(每个字节)都进行了编号,就好比每个住户都有一个门牌号。编号是从0开始的,内存中的第一个字节被编号为0,第二个字节被编号为1,依次类推。假设内存总共有65536字节的储存空间,那么从第一个字节到最后一个字节会分别被编号为0,1,2,3,...,65535,这个编号就叫做内存地址(Memory Address),或者简称为地址(Address)。内存地址一般会被记作十六进制数,所以上面的例子里面的内存地址会写为:0x0001, 0x0002,...,0xFFFF。
在星际eud领域内讨论内存时,我们一般会将内存地址记为一个8位的十六进制数,即:
0x00000000,
0x00000001,
0x00000002,
...
0x0000000F,
0x00000010,
0x00000011,
...

注意!内存地址是内存中存储着的数据的一个标识,并不是数据本身,通过内存地址可以找到内存当中存储的数据。
在实际情况中,计算机内通常会有多个程序同时运行,同时占用内存,因此每个程序都会独自占用一块属于它自己的内存空间,不同程序一般互不干扰。所以,我们在讨论某个特定程序时(比如星际争霸游戏),会以本程序所占用的第一个字节的地址为基准,称它的地址为0,剩下的其他字节依次编号1,2,3,...。EUD内存表中的所有地址,以及本文之后的内容中所提到的所有的星际争霸游戏内的内存地址,其实都是相对的,并且我们对绝对的地址不感兴趣。
让我们来看下面这个例子:
下图中每个字节所在地址是用十六进制表示的:

p03

p03

下图中每个字节所在地址是用十进制表示的:

p04

p04


我们可以看到,第一个字节的地址为0,或者写为0x00000000,这个字节所储存的数据为0x4D,即0b01001101,第二个字节所储存的数据为0x50,即0b01010000。地址为0x0000002F的字节所储存的数据为0x27,地址0x00000051内储存的数据为0x28。

在有些情况,我们可能会以某个内存地址为基准,去定位其他内存,此时,那个作为基准的内存地址被称为基址(base address),而其他内存相对于基址的距离叫做偏移或者偏移量(offset),这是一个很重要的词汇,之后你会在eud的各种资料里面见到它。比如,如果以0x0000002F这个地址为基址,那么0x0000002E这个地址是基址的上一个字节,因此0x0000002E的偏移就是-0x000000001,当我们讨论的内存范围较小时,可以用较少位数来写,比如写成-0x001。那么基址的下一个地址,0x00000030的偏移就是+0x001。基址之前的地址的偏移量均为负数,基址之后的地址的偏移量均为正数。

习题:
(1)一个byte等于多少bit?
(2)4字节的储存空间总共有多少种可能的状态?
(3)每个内存地址所对应储存空间有多大?
(4)本节的示例图片中,地址0x0000017A所储存的数据是什么?
(5)以0x59CCA8为基址,则0x59CD0C的偏移量为多少?


回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-1 03:29:22 | 显示全部楼层
本帖最后由 PereC 于 2020-12-1 15:58 编辑

【第三节】数据的读取与写入
一个字节可以有256种不同的状态,所以我们如果使用一个字节去储存一个非负整数,那么我们可以储存0至255之间的所有整数(含0和255),我们可以规定:当这个字节的状态为00000000(即0x00)时,它所储存的就是0这个整数;当它的状态为00000001(即0x01)时,它所储存的就是1这个整数;......;当它是0xFF时,它所储存的就是255这个整数。为了保证信息在读取时与写入时的一致性,我们显然要规定:这个字节的不同状态与它所表示的信息是一一对应的关系,也就是说,我们不可能让整数0和1同时对应0x00这个数据,也不可能让0x00和0x01这两个数据同时对应整数0,即这256种不同的状态分别对应256种不同的信息,并且在这个例子中,对应关系如下:
0x00对应的是整数0
0x01对应的是整数1
...
0xFF对应的是整数255

当然,如果你想的话,也可以制定如下规则:
0x00对应的是255
0x01对应的是254
...
0xFF对应的是0

当然,你还可以制定如下规则:
0x00对应0
0x01对应1
0x02对应2
...
0x7F对应127
0x80对应-128
0x81对应-127
...
0xFE对应-2
0xFF对应-1

甚至你还可以让一个字节去储存256个不同的偶数。
显而易见,你有无数种方法去制定规则,但是无论你用哪种规则,一个字节如果用来储存整数,都只能储存256个不同的整数。所以,如果你想储存较大的数,你就要使用更多的字节。

内存中的数据读取与写入的两大要素:
(1)所要读取或写入的数据的地址
(2)读取或写入数据的规则:包括该数据的长度,数据与其所表示的信息的对应关系(见上面两个例子),使用何种字节序(endian)等。

下面我们介绍几种常用的读写规则:
1. 单字节非负整数(8-bit unsigned integer, 简称uint8或者u8)
这种规则可以让一个字节储存0至255之间的所有整数,具体读写规则见上面的第一个例子,在此不再赘述。
假设我们要往地址0x0000FFF0内写入127这个数据,使用单字节非负整数(uint8)的规则,则我们会将0x0000FFF0内的数据改为0x7F。注意,数据的写入是涂抹性的,当我们决定在某个地址内写入新数据时,无论这个地址内原本的数据是什么,我们都会将新数据直接写入,覆盖掉原有数据。
同理,如果我们要在地址0x0000FFF0读取一个uint8,假设0x0000FFF0地址里储存的数据为0x7F,那么我们读取的结果就是127
注:在计算机领域内,非负整数的学名是unsigned integer,应当直译为无符号整数,我在此使用“非负整数”方便大家理解。

2. 双字节非负整数(16-bit unsigned integer, 简称uint16或u16)
用两个字节来储存一个非负整数,显然我们就可以储存0至65535之间的所有整数了。储存这个整数的这两个字节是相邻的两个字节,并且我们用靠前的那个地址来作为这个数所在的地址。比如我们要用0x0000FFF0和0x0000FFF1这两个字节来储存这个双字节整数,则我们称:储存这个数的地址为0x0000FFF0

储存时,首先我们将这个整数写为一个4位的十六进制数。由于这个数据占用的是两个字节,就牵扯到了字节序(endian)的问题。字节序分为大端序(big endian)小端序(little endian),它们的区别为:
我们现在要储存127这个双字节非负整数,那么首先将它转化为十六进制数0x007F,如果我们要把它储存在0x0000FFF0这个内存中,则如果按照大端序(big endian)的方式,则会在0x0000FFF0中储存0x00,在0x0000FFF1中储存0x7F。如果按照小端序(little endian)的储存方式,则会在0x0000FFF0中储存0x7F,在0x0000FFF1中储存0x00。

大端序(big endian)即高字节优先
0x0000FFF0: 0x00
0x0000FFF1: 0x7F

小端序(little endian)即低字节优先
0x0000FFF0: 0x7F
0x0000FFF1: 0x00

读取数据时也是同理。假设目前内存中储存的数据如下:
地址0x0000FFF0储存的数据为0x00
地址0x0000FFF1储存的数据为0x7F
地址0x0000FFF2储存的数据为0x12
地址0x0000FFF3储存的数据为0x34
那么我们现在如果在地址0x0000FFF0处以big endian读取一个双字节非负整数,那么我们读取到的结果就是0x007F,即127。如果在地址0x0000FFF0处以little endian读取一个双字节非负整数,那么我们读取到的结果就是0x7F00,即32512。如果在地址0x0000FFF1处以little endian读取一个双字节非负整数,那么我们读取到的结果就是0x127F,即4735。

可以看到,我们在写入数据和读取数据时必须使用完全一致的规则,且读取的位置与写入的位置是同一个地址,才能保证我们读取的数据跟我们写入的数据是一样的,否则就会天差地别


3. 四字节非负整数(32-bit unsigned integer, 简称uint32或u32)
可储存的范围是0至4294967295之间的所有整数,其它与双字节非负整数类似,也有little endian和big endian两种字节序,在此不做赘述,只举两例:
将12345678以四字节非负整数(little endian)的形式储存到0x0000FFF0中,得到的结果是:
地址0x0000FFF0储存的数据为0x4E
地址0x0000FFF1储存的数据为0x61
地址0x0000FFF2储存的数据为0xBC
地址0x0000FFF3储存的数据为0x00
解释:12345678表示为十六进制是0x00BC614E,按照低字节优先的方式储存。

将127以四字节非负整数(little endian)的形式储存到0x0000FFF0中,得到的结果是:
地址0x0000FFF0储存的数据为0x7F
地址0x0000FFF1储存的数据为0x00
地址0x0000FFF2储存的数据为0x00
地址0x0000FFF3储存的数据为0x00
解释:127表示为十六进制是0x0000007F,按照低字节优先的方式储存,即第一个字节储存0x7F,后面3个字节都储存0x00。

在这个例子中我们可以看到,使用little endian的好处:在读取0x0000FFF0中的数据时,无论你读取的是单字节整数、双字节整数还是四字节整数,你读取到的都是127这个数。
注意:星际争霸1中所有整数型数据的储存方式皆为小端序(little endian),低字节优先!故下文全部默认是little endian
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-1 03:29:46 | 显示全部楼层
本帖最后由 PereC 于 2020-12-14 16:51 编辑

4. 有符号整数(signed integer)
单字节有符号整数(8-bit signed integer, 简称s8)
储存的范围是-128至127之间的所有整数,共256个整数。规则为:
0x00对应的是整数0
0x01对应的是整数1
0x02对应的是整数2
...
0x7F对应的是整数127
0x80对应的是整数-128
0x81对应的是整数-127
...
0xFE对应的是整数-2
0xFF对应的是整数-1
注:此规则名为二补数(two's complement)规则,星际中储存的有符号整数皆使用该规则。

双字节和三字节略。

四字节有符号整数(32-bit signed integer, 简称s32)
储存的范围是-2147483648至2147483647之间的所有整数,共2^32个整数。也是使用二补数规则,即为:
0x00000000: 0
0x00000001: 1
...
0x000000FF: 255
0x00000100: 256
...
0x7FFFFFFF: 2147483647
0x80000000: -2147483648
0x80000001: -2147483647
...
0xFFFFFFFF: -1
例:
向0x0000FFF0写入一个s32(little endian)数值-2147483647,得到的结果是:
地址0x0000FFF0储存的数据为0x01
地址0x0000FFF1储存的数据为0x00
地址0x0000FFF2储存的数据为0x00
地址0x0000FFF3储存的数据为0x80
解释:根据s32规则,-2147483647对应的数据是0x80000001,按照低字节优先的方式储存,第一个字节储存0x01,中间两个字节储存0x00,第四个字节储存0x80。

例:内存状态如下时:
地址0x0000FFF0: 0x01
地址0x0000FFF1: 0x00
地址0x0000FFF2: 0x00
地址0x0000FFF3: 0x80
从地址0x0000FFF0读取一个s32(little endian),读取的结果是:
首先用little endian的规则来获取数据,获取到的数据为0x80000001,然后按照s32的规则,这个数据所代表的数是-2147483647,即为读取结果。

5. 布尔变量(Boolean variable)
布尔变量又称逻辑变量,取值只能为True(真)或者False(假),因此它只需要占用1bit的储存空间:
0: False
1: True
布尔变量是本文中唯一一个只占用1bit空间的变量,其它变量所占用的空间都是以字节为单位的。星际中最典型的布尔变量就是Switch的开关状态。“开(Set)”就是True,对应的比特值是1,“关(Cleared)”就是False,对应的比特值是0。在星际游戏刚开始时,所有开关全部处于默认的关闭状态,即0,之后通过触发来改变。
一个内存地址代表一个字节,一字节等于8bit,因此可以储存8个布尔变量。在星际中,地址0x0058DC40储存的是Switch1至Switch8这8个switch的开关状态,也遵循little endian原则,即低位优先,即0x0058DC40内所储存的数据的最低位是Switch1的开关状态,最高位是Switch8的开关状态。
若0x0058DC40储存的数据为0xB3,则:
0xB3 = 0b10110011
我们可以得知,此时Switch1,2,5,6,8处于Set状态,而Switch3,4,7处于Cleared状态。
目前为止,我们的读写精度都仅限于字节,而布尔变量需要精确到bit,故在此暂不介绍其读写方式。


6. 字符(character)和字符串(string)
字符和字符串的特点是长度不固定,这部分内容将会在另一个教程里面详细介绍

7. 指针(Pointer)
指针(指针变量)的读写标准等同于u32,也是一个四字节的非负整数,但是其储存的数据含义为内存地址。比如内存0xAABBCC00储存着一个指针变量,其值为0x0059CCA8,即:
0xAABBCC00: 0xA8
0xAABBCC01: 0xCC
0xAABBCC02: 0x59
0xAABBCC03: 0x00
则我们称:0xAABBCC00储存的这个指针变量指向内存地址0x0059CCA8
当指针储存的值为0时,我们称它为空指针(Null Pointer)
指针变量对数据的读写、内存的分配有着至关重要的作用,不过这不是本节内容的重点,故暂不做详细介绍。以后可能会在科普UnitNode结构时做详细介绍。注:UnitNode Table储存着地图上1700个单位的详细信息,是一个双向链表,unit数据的读写皆由指针控制。


习题:
1. 在某个内存地址中写入新数据,是否会覆盖掉这个地址内的原有数据?
2. 如果我们在内存0x00AABBCC中以little endian的方式写入了一个四字节非负整数(u32),那么我们在读取这个信息时,应该在哪个内存读取?以什么规则读取?
3. 假设内存中储存着如下数据:
地址0x00FF8800: 0x7F
地址0x00FF8801: 0x00
地址0x00FF8802: 0x80
地址0x00FF8803: 0x00
(1)从0x00FF8800读取一个u8(little endian),读取到的数是什么?
(2)从0x00FF8800读取一个u16(little endian),读取到的数是什么?
(3)从0x00FF8800读取一个u32(little endian),读取到的数是什么?
(4)从0x00FF8801读取一个u16(little endian),读取到的数是什么?
(5)从0x00FF8801读取一个s16(little endian),读取到的数是什么?
(6)从0x00FF8802读取一个s16(little endian),读取到的数是什么?
(7)向0x00FF8800以u16(little endian)的方式写入数值128后,写出以上四个地址的新状态。
(8)向0x00FF8800以u32(little endian)的方式写入数值128后,写出以上四个地址的新状态。
(9)向0x00FF8803以u16(little endian)的方式写入数值128后,写出以上四个地址的新状态。
(10)若地址0x00FF8800储存的是一个指针,则该指针指向的地址为?
4. 已知地址0x00AABB00中储存的数据是0x20,且从0x00AABB00读取u8、u16、u32所得到的数都一样。那么请问读取到的数是什么?读取方式为little endian还是big endian?0x00AABB01、0x00AABB02、0x00AABB03这三个地址中储存的数据是什么?
5. 星际中一共有256个可用的Switch,储存它们(的开关状态)至少需要多少字节?




回复 支持 反对

使用道具 举报

发表于 2020-12-5 11:04:24 | 显示全部楼层
似乎学到了点什么~~~
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-10 11:35:57 | 显示全部楼层
本帖最后由 PereC 于 2020-12-11 11:52 编辑

【第四节】数组(Array)
数组(Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。以上定义摘自维基百科。简单来讲,数组就是一组数,每个数是同类型的,具有完全相同的特征。比如,一个班级内每个学生的考试成绩,一个公司内每个员工的薪水等等。数组的一个重要特征就是长度(length),即它所含有的元素个数。一个数组所占用的内存空间等于它的单个元素所占用的内存空间乘以这个数组的长度。如果我们将一个长度为n的数组命名为a,则它的第一个元素叫做a[0],第二个元素叫做a[1],...,第n个元素叫做a[n-1]。其中,中括号内的0,1,...,n-1就叫做元素的索引(index)一个数组所在的地址一般是指它的第一个元素所在的地址。
下面用星际游戏中的游戏数据来举例:
每个玩家的实时水晶矿数就是一个数组,确切来讲,是一个长度为12的数组,每个元素都是一个u32数据(little endian就不用说了),该数组所在的地址为0x0057F0F0,占用的总内存为4*12=48字节。如果将此数组命名为mineral,则:
mineral[0]为Player1的水晶矿数,储存地址为0x0057F0F0
mineral[1]为Player2的水晶矿数,储存地址为0x0057F0F4
mineral[2]为Player3的水晶矿数,储存地址为0x0057F0F8
mineral[3]为Player4的水晶矿数,储存地址为0x0057F0FC
mineral[4]为Player5的水晶矿数,储存地址为0x0057F100
...
mineral[11]为Player12的水晶矿数,储存地址为0x0057F11C
显而易见,mineral[k]所在内存为0x0057F0F0+4*k,其中k为0至11间的整数。
所以,对于任意一个数组,索引为k的元素所在地址相对于该数组所在地址的偏移量为:单个元素所占的空间大小乘以k
即:若数组a的长度为n,每个元素大小为s,则元素a[k]相对于数组a地址的偏移量为ks,其中k=0,1,2,...,n-1

星际中有很多很多这种长度为12的数组,记录着每个玩家的某个数据。有的时候,Player9至Player12这4个玩家是没用的,所以有的数组长度仅为8。考虑到数组的索引是从0开始的,所以我们有时会使用玩家编号(player ID)来指代玩家:
Player1的玩家编号(player ID)为0
Player2的player ID为1
...
Player12的Player ID为11
所以,playerID为k的玩家的水晶数所在内存地址为0x0057F0F0+4k
注意,playerID是一个极其重要的概念,之后会经常使用,它不是指玩家的游戏名ID,而是玩家的编号。playerID中的ID可以理解为index。

二维数组(2d Array)
二维数组又可称为矩阵(Matrix),其储存的数据一般可以被列为一个表格,有行和列。二维数组储存的数据有两个维度,因此定位一个元素需要两个index。如果一个二维数组的两个维度分别为m和n,则我们称它为一个m×n数组,将它写为一个表格则有m行、n列。注意,m和n的顺序不能互换。每个元素都有一个行索引(row index)列索引(column index)。单位死亡数(Death Table)就是一个典型的二维数组:
UnitID\PlayerID0 (Player1)1 (Player2) 2 (Player3)3 (Player4) 4 (Player5) 5 (Player6) 6 (Player7) 7 (Player8) 8 (Player9) 9 (Player10) 10 (Player11) 11 (Player12)
0 (Terran Marien) 0 08 0 6 0 0 1 0 0 00
1 (Terran Ghost) 00 0 0 0 0 0 0 0 0 0 0
2 00 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 0 0
5 00 0 0 0 0 0 00 0 0 0
... ...
226...










227 ...

可以看到,这个表格有228行(星际一共有228种单位)和12列(总共有12个玩家),所以这个数组是一个228×12的数组,它所含有的元素个数为228×12=2736,每个元素都是一个u32(四字节非负整数)。如果我们将这个二维数组命名为DeathTable,则我们用DeathTable[x][y]或者DeathTable[x, y]来表示它第x+1行、第y+1列所对应的元素。比如DeathTable[0][0]表示的就是第一行第一列的元素,即玩家1的Terran Marine死亡数,DeathTable[0][1]代表玩家2的Terran Marine死亡数,DeathTable[0][2]代表玩家3的Terran Marine死亡数,DeathTable[1][0]代表玩家1的Terran Ghost死亡数。中括号内的数字就是元素的索引(index),二维数组有两个维度,所以需要两个索引才能定位一个元素,第一个index是行的index,第二个index是列的index,在DeathTable中,第一个index就是单位编号(unitID),第二个index就是玩家编号(playerID)
二维数组在内存中的储存方式为:把每一行都当成一个普通数组来储存,用连续的内存来储存第一行,然后紧接着储存第二行,以此类推。一行一行地储存,而不是一列一列地储存。DeathTable中的2736个元素的储存顺序为:
DeathTable[0][0], DeathTable[0][1], DeathTable[0][2], DeathTable[0][3], ..., DeathTable[0][11], DeathTable[1][0], DeathTable[1][1], ..., DeathTable[227][10], DeathTable[227][11]
如果我们把这2736个元素视为一个普通的一维数组,并命名为a,则DeathTable[0][0]为a[0],DeathTable[0][1]为a[1],DeathTable[0][2]为a[2],...,DeathTable[1][0]为a[12],DeathTable[1][1]为a[13],...,DeathTable[227][10]为a[2734],DeathTable[227][11]为a[2735]。
很容易找到规律:DeathTable[x][y]为a[12x+y]
我们将二维数组所在的内存地址定义为它的第一行第一列的元素所在的地址,即元素[0][0]所在的地址。DeathTable所在的地址为0x0058A364,即DeathTable[0][0]所在地址为0x0058A364,根据二维数组的储存特点,我们可知DeathTable[0][1]所在地址为0x0058A368,以此类推,可知:
DeathTable[x][y]所在地址为0x0058A364 + 4(12x + y),即0x0058A364 + 48x + 4y
上面这个结论是一定要记住的,它可以被称为eud这座大厦的理论基石。
举例:
问:P4的Terran Medic死亡数所在内存地址为多少?
答:P4的playerID为3,Terran Medic的unitID为34,因此我们要找的是DeathTable[34][3]的地址,根据公式可得:
0x0058A364 + 4(12*34 + 3) = 0x0058A9D4
即P4的Terran Medic死亡数所在内存地址为0x0058A9D4

所以:
若二维数组a为一个m×n数组,每个元素大小为s,则元素a[x][y]所在的地址相对于数组a所在地址的偏移量为(mx+y)s,其中x=0,1,2,...,m-1,y=0,1,2,...,n-1

习题:
(1)一个数组所在的内存地址为0x00AA00,数组长度为60,每个元素都是u16,则数组中的第一个数的index是多少?最后一个数的index是多少?这个数组总共占用多少字节的内存?数组的第10个元素所在地址为?index为20的元素所在地址为?
(2)内存0x57F120储存的是每个玩家的实时气体数量,是一个长度为12的数组,每个元素都是u32。已知在某一时刻,该数组的所有元素的值都是0,之后玩家4采集了16气体,其他玩家没有采集任何气体。请问,玩家4采集了16气体后,具体哪个地址(字节)内的数据发生了变化?变成了什么(以十六进制数表示)?
(3)已知二维数组a是一个30×12数组,每个元素都是u32,则a一共含有多少个元素?a总共占用多少字节?a[3][4]所在的地址相对于a的地址的偏移量为多少?
(4)playerID为p的玩家的枪兵死亡数所在的内存地址是多少(用p表示)?玩家3的unitID为k的单位的死亡数所在的内存地址是多少(用k表示)?





回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-12 13:54:56 | 显示全部楼层
本帖最后由 PereC 于 2020-12-22 02:17 编辑

【第五节】初识eud,了解内存表
一个长度为n的一维数组的索引的取值范围是0,1,2,...,n-1。一个m×n的二维数组的行索引取值范围为0,1,2,...,m-1,列索引的取值范围为0,1,2,...,n-1。显然,索引在取值范围内才可以表示这个数组内的元素。那么,如果索引的值在取值范围之外,会发生什么呢?比如一个长度为10的数组a,其10个元素为a[0], a[1], ..., a[9]。那么a[-1]是谁?a[10]又是谁呢?
这种索引超出其合法取值范围的情况,称为index out of range,一般可能会导致程序运行出错,所以一个编程严谨的程序是绝对不会允许这种情况发生的。但是,如果程序允许index out of range,则a[-1]和a[10]可以读取到数据,具体读取到什么数据,代入公式就行了。公式在上一节已经介绍:
若数组a的长度为n,每个元素大小为s,则元素a[k]相对于数组a地址的偏移量为ks,其中k=0,1,2,...,n-1
若二维数组a为一个m×n数组,每个元素大小为s,则元素a[x][y]所在的地址相对于数组a所在地址的偏移量为(mx+y)s,其中x=0,1,2,...,m-1,y=0,1,2,...,n-1
若我们允许index out of range,即不再限制其取值范围,则上述公式的k和x,y可以取任何整数值,都可以读到数据。
比如数组a所在地址为0xAABB08,数组长度为10,每个元素都是u32(大小为4字节),则元素a[-1]相对于数组a地址的偏移量为-4,所以读取a[-1]就等同于在0xAABB04读取一个u32,读取a[10]就等同于在0xAABB08+40=0xAABB30读取一个u32。你甚至可以读取a[-124198],a[8935239],.....。所以,以一个数组为基址,通过改变index的值,可以读取到所有内存地址内的数据。

EUD的全称为Extended Unit Death,中文直译为拓展单位死亡数。其中,“拓展”的含义就是拓展索引,即index out of range。星际触发中有一个condition叫做Deaths(......),有一个action叫做SetDeaths(...),比如:

p05

p05


相应的TEP代码为:
Trigger {
        players = {P1},
        conditions = {
                Deaths(P1, AtLeast, 3, "Terran Marine");
        },
        actions = {
                SetDeaths(P4, Add, 5, "Terran SCV");
        },
}
如果把P1、P4和单位的名称改成PlayerID和UnitID的形式,则可以写成:
Trigger {
        players = {P1},
        conditions = {
                Deaths(0, AtLeast, 3, 0);
        },
        actions = {
                SetDeaths(3, Add, 5, 7);
        },
}

我们知道,正常情况下,playerID只能是0至11中的一个整数,unitID只能是0至227中的一个整数。但是现在,我们尝试让playerID或者unitID突破这个限制。比如将Deaths的第一个参数(playerID)强行设置为100:

p06

p06

输入完毕后,你会发现它变成了:

p07

p07

其实这也没什么稀奇的。我们知道:
Deaths(100, AtLeast, 3, 0)的含义为:
playerID:100的玩家的unitID:0单位的死亡数至少为3
即:
DeathTable[0][100]的值至少为3
即:
内存地址0x0058A364 + 4(12*0 + 100) = 0x0058A4F4所储存的u32数据的数值大于等于3

其实,对任意给定的整数u和p:
Deaths(p, AtLeast, 3, u)的含义为:
DeathTable[u ][p]的值至少为3
即内存地址0x0058A364 + 4(12u + p)所储存的u32数据的数值大于等于3

上面这个例子中,我们将u的取值定为0,p的取值定为100,所以是去读取0x0058A4F4的值,这种将p的取值定为其合法范围(0-11)之外的行为,叫做Extended Player Death,简称EPD。当然,你也可以将u的取值定为0至227以外的整数,这种将u的取值定为其合法范围(0-227)之外的行为叫做Extended Unit Death,简称EUD

再举一个例子:
SetDeaths(30, Add, 5, 1000)的含义为:
将DeathTable[1000][100]的u32数据的值加5
即:将地址为0x0058A364 + 4(12*1000 + 100) = 0x00596074内的数据的值+5

很显然,EPD和EUD的目的完全相同,都是为了读取或修改某个特定的内存地址内的数据,他俩也是完全等价的。我们可以令u=0,然后任取p的值,就可以任意读取或修改所有4的倍数的内存地址中所储存的u32数据。即,令u=0:
0x0058A364 + 4(12u + p) = 0x0058A364 + 4(12*0 + p) = 0x0058A364 + 4p
所以,一般的eud触发都是默认u=0,即Terran Marine的死亡数,然后任意指定p的值,从而读取或修改特定的内存。本质上来讲,这个应该叫做EPD,但是大家叫EUD叫习惯了,所以就这么一直叫下来了。
以下是p的取值与内存的对应关系:
...
0x0058A35C: p=-2
0x0058A360: p=-1
0x0058A364: p=0
0x0058A368: p=1
0x0058A36C: p=2
...
我们可以看到,每一个4的倍数的内存地址(a)都与一个p值一一对应,我们将这个p值称为playerID或者EPD值,它们的对应关系为:
a = 0x0058A364 + 4p
p = (a - 0x0058A364)/4
因此,我们可以定义一个名为EPD的函数:
EPD(a) = (a - 0x0058A364)/4,其中a为任意一个4的倍数的内存地址。
由于内存地址写起来很冗长,所以我们经常以EPD值来表示一个内存地址,比如0x0058A364的EPD值就是0,0x0058A35C的EPD值就是-2

由于DeathTable里面的数据都是u32,即四字节非负整数,所以使用eud(或者epd)所读取或写入的数据也必须是u32,所涉及的地址也必须是4的倍数。即,无论这个内存里的数据本身是什么类型的(可能是u16, s32, 甚至string),只要是用eud技术来读取,那么我们只能以u32来读取;同理,写入也只能写入一个u32。至于如何读取非4的倍数的内存里的数据,或者如何以非u32的方式来读取、写入数据,将会在之后的bitmask章节中详细介绍。

其实,我们可以直接在最新版scmd的触发列表中找到eud相关的触发:
Condition里面的两个:
EUD: Memory Value和EUD: Memory Value (Masked)
Action里面的两个:
EUD: Modify Memory和EUD: Modify Memory (Masked)

随便点一个之后,会显示Deaths或者SetDeaths的条件/动作,此时你可以将playerID栏内写入任意一个整数(不要写0至27中的整数),它就会自动转化为内存地址。当然,我还是建议各位直接使用TEP的Memory()和SetMemory()来写,比如:
Trigger {
        players = {P1},
        conditions = {
                Memory(0x59CCF4, Exactly, 1);
        },
        actions = {
                SetMemory(0x57F0F0, Add, 100);
        },
}
含有至少一个Memory条件或者至少一个SetMemory动作的触发,叫做EUD触发。上面这个就是一个典型的eud触发。本质上来讲,星际里面的所有触发都可以写成eud触发。因为所有触发的本质都是读取某个内存的数值,根据这个数值(条件),来决定是否要修改某个内存内的数值(动作)。官方给定的预设触发(不含eud触发)中若干个动作和条件,都只能读取很小一部分的内存,或者修改很小一部分的内存。如果我们需要读取其他内存,或者修改其他内存,则必定需要使用eud触发。
定义:
使用至少一条eud触发的地图,叫做eud地图。
重制版星际的eud地图有如下特点:
(1)无法使用单位拓展,即单位上限为原版的1700,而不是3400
(2)无法在游戏中保存游戏
(3)游戏结束后无法保存录像


eud地图的这3个致命缺点,可以换取更自由的设计,实现普通地图所不能实现的种种功能。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-12 13:58:19 | 显示全部楼层
本帖最后由 PereC 于 2020-12-27 18:01 编辑

了解了这些知识点后,内存表就变得很容易理解了。星际游戏中的大多数游戏数据都有固定的内存,比如单位死亡数这个二维数组永远储存在0x0058A364,玩家的实时水晶矿数量永远储存在0x0057F0F0等等等等。星际1的前辈们通过实践,将几乎所有的重要游戏数据所在的内存地址全部找到,并总结成一个列表,这就是星际1的内存表(Memory table),是eud制图的重要参考资料。
打开内存表(链接在本文开头),我们可以看到Address、Player ID、Version等表头信息:

p08

p08


Address当然就是代表内存地址,不做过多的解释。
Player ID即该内存所对应的EPD值
Version就是指当前内存表所适用的星际版本,目前看到的都是1.16,即该内存表适用于1.16版本的星际。星际重制版的内存表与1.16版本的基本相同。
Name就是告诉我们这个内存里面储存的数据的含义
Size和Length: 星际里面的所有数据都可以看成是数组,其中size代表数组中每个元素所占用的空间(单位为字节),length代表数组的长度。比如0x0057F0F0储存的是Player Mineral,就是一个size为4(每个元素都是u32,占用4字节,)、length为12的数组,总共占用48字节的空间。比如0x006509B0储存的是CurrentPlayer值,size是4,length为1,说明它就是一个普通的独立的4字节数据(也是一个u32)。
SCR: 意思是该内存是否被重制版支持。Simple Data、Support都是在重制版可以读写的内存,Backed By Code我不太清楚。Read Only是只读,即只能读取这个内存的值,但是不允许写入,一旦尝试向该内存写入内容,游戏直接崩溃。Unsupported就是不支持,即不能读也不能写,一旦尝试读写则游戏崩溃。这些都仅供参考,具体还要靠自己试验。毕竟这些也是前人试验出来的结果,不是暴雪官方给的。
Description: 即备注、详细解释。点击左边蓝色的内存地址,即可查看相应Description的全部内容。

作者们没有灵感的时候,就可以去看看内存表,看看有哪些游戏数据是可供修改的。
内存表中有一些特定的结构,比如CUnit, CSprite等等,将在之后的内容介绍。
下面举一例:
在内存表中找到地址0x58DC60,即Location Table,可以看到它的size为20,length为255,可知:
整个Location Table所占用的空间为20*255=5100字节,地图中总共有255个location,每个location的信息占用20字节。点击左边蓝色的0058DC60,即可查看详细说明。无法翻墙的读者可直接看以下截图:

p09

p09


p10

p10


p11

p11



详细说明的内容解释了这20个字节代表了什么信息:
前16字节储存了4个u32,分别是该location的左边界横坐标(L)、上边界的纵坐标(U)、右边界的横坐标(R)、下边界的纵坐标(D),单位为像素(Pixel)
最后的4字节中的前两个字节(u16)储存的是这个location的名字string的stringID,这个概念会在以后的string教程中介绍;后两字节储存的是这个location的各种flag,每个flag占用1bit,总共只有6个flag,所以这些flag只会占用6bit(最低的6bit),剩下的高位10bit都是0。其中,LowGround为最低位,剩5个flag,以此类推。
比如,内存中储存的数据如下:

内存地址 数据 内存地址 数据 内存地址 数据
0x0058DC60 0xC0 0x0058DC74 0xC0 0x0058DC88 0x00
0x0058DC61 0x07 0x0058DC75 0x07 0x0058DC89 0x00
0x0058DC62 0x00 0x0058DC76 0x00 0x0058DC8A 0x00
0x0058DC63 0x00 0x0058DC77 0x00 0x0058DC8B 0x00
0x0058DC64 0x40 0x0058DC78 0x20 0x0058DC8C 0x00
0x0058DC65 0x07 0x0058DC79 0x08 0x0058DC8D 0x00
0x0058DC66 0x00 0x0058DC7A 0x00 0x0058DC8E 0x00
0x0058DC67 0x00 0x0058DC7B 0x00 0x0058DC8F 0x00
0x0058DC68 0x60 0x0058DC7C 0x60 0x0058DC90 0x00
0x0058DC69 0x08 0x0058DC7D 0x08 0x0058DC91 0x00
0x0058DC6A 0x00 0x0058DC7E 0x00 0x0058DC92 0x00
0x0058DC6B 0x00 0x0058DC7F 0x00 0x0058DC93 0x00
0x0058DC6C 0xC0 0x0058DC80 0xA0 0x0058DC94 0x00
0x0058DC6D 0x07 0x0058DC81 0x08 0x0058DC95 0x00
0x0058DC6E 0x00 0x0058DC82 0x00 0x0058DC96 0x00
0x0058DC6F 0x00 0x0058DC83 0x00 0x0058DC97 0x00
0x0058DC70 0x08 0x0058DC84 0x09 0x0058DC98 0x00
0x0058DC71 0x00 0x0058DC85 0x00 0x0058DC99 0x00
0x0058DC72 0x28 0x0058DC86 0x00 0x0058DC9A 0x00
0x0058DC73 0x00 0x0058DC87 0x00 0x0058DC9B 0x00


根据这些数据,我们可以知道:
0x0058DC60至0x0058DC73这20个字节储存的是整个地图中第一个location的信息:
这个location的4个边界坐标分别为0x000007C0, 0x00000740, 0x00000860, 0x000007C0,即1984,1856,2144,1984
这个location的名字的stringID为0x0008,即8
它的flag状况为0x0028,即0b00101000,即第4个和第6个flag的状态为1,所以这个location在scmd中为:

p12

p12


同理可知,地图中的第二个location的信息为:

p13

p13


注意,用eud读取内存时只能在4的倍数的内存地址读取u32,所以,我们获取第一个location的stringID或者flag信息时只能在0x0058DC70读取u32,这个数值包含了stringID和flag信息。这个u32有32bit,而6个flag分别位于这32bit的第17、18、19、20、21、22位,所以如果第一个flag值为1,它将为这个u32的数值贡献2^(17-1)=2^16=65536,第二个flag值为1则会为这个u32的数值贡献2^(18-1)=131072以此类推。这也就是为什么内存表对于location的详细说明中的flags后面写了这些数。
在实际操作中,我们无法看到内存中每个字节的数据,只能通过读取4的倍数的内存地址内的u32来获取数据。因此,在这个例子中,我们读取0x0058DC70的数据会得到2621448这个数值,我们要其转化为十六进制数0x00280008才可更直观地解读它的意义。


这里顺带提一下,星际中的每个location也有编号,这个编号称为locationID,只不过location的编号是从1开始的,而不是从0开始的。第一个location的locationID为1,第二个location的locationID为2,以此类推。"Anywhere"是默认存在的一个location,其ID为64。locationID为k的location的信息所在的内存地址相对于Location Table所在地址的偏移量为20(k-1)

习题:
1. EUD和EPD的全称分别是?二者在功能与目的上是否等价?
2. 触发中的条件Memory与动作SetMemory所能读写的数据类型为?
3. 判断正误:EUD地图必然含有至少一条EUD触发。
4. 相对于非EUD地图,EUD地图的劣势是什么?
5. 函数EPD(a)的自然定义域为?(a的合法取值范围是什么?)
6. 下列action中,合法的是哪些?
(1) SetMemory(0x6615A8, SetTo, 0);
(2) SetMemory(0x6615A9, SetTo, 0);
(3) SetMemory(0x6615AA, SetTo, 0);
(4) SetMemory(0x6615AB, SetTo, 0);
(5) SetMemory(0x6615AC, SetTo, 0);
7. EPD(0x58A364)的值为多少?EPD值-2000所对应的内存地址为?储存"Anywhere"这个location的信息的内存地址为?
8. 已知下面两个触发完全等价。填空。
Trigger {
        players = {P1},
        conditions = {
                Deaths(P4, AtLeast, 3, "Terran Marine");
        },
        actions = {
                SetResources(P2, SetTo, 100, Gas);
        },
}

Trigger {
        players = {P1},
        conditions = {
                Memory(____, AtLeast, ____);
        },
        actions = {
                SetMemory(____, SetTo, ____);
        },
}
提示:回忆第四节讲到的每个玩家的实时水晶矿数是如何在内存中储存的,并在内存表中找到储存玩家实时气体的地址。
9. 若尝试在地址0x00000000读取或写入数据,会发什么事?(请使用scmd自行试验)

回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-17 00:56:17 | 显示全部楼层
本帖最后由 PereC 于 2020-12-21 17:19 编辑

【第六节】位掩码(Bit Mask)
本节主要介绍scmd触发中的"EUD: Memory Value (Masked)"条件和"EUD: Modify Memory (Masked)"动作。

Memory()和SetMemory()只能读写u32数据,本节将介绍的位掩码(Bit Mask)可以用来协助读写各种长度的数据,包含u16, u8, 甚至是Boolean等。使用的触发条件/动作为MemoryX()和SetMemoryX(),在scmd的Cllassic Map Triggers界面为"EUD: Memory Value (Masked)"条件,和"EUD: Modify Memory (Masked)"动作。

首先来介绍位逻辑运算(logical operation)。三个基本的逻辑运算符(logical operator):"与(And)"、"或(Or)"、"非(Not)"
1bit的值只可能是0或1,定义1为真(True),0为假(False),即可定义以下逻辑运算:
与(And),二元运算符,用符号表示为"&"
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1

或(Or),二元运算符,用符号表示为"|"(一个竖线)
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1

非(Not),一元运算符,用符号表示为"¬"或者"!"或者"~"
!0 = 1
!1 = 0

这些定义都是符合逻辑的,不做过多解释。
位操作(Bitwise operation),或称位运算,是指将整数写成二进制形式,在bit基础上进行一些操作。这里仅介绍“按位与(bitwise And)”运算,符号也为&:
bitwise And运算的法则是将两个运算子写成二进制形式,低位对齐(高位bit填充0),然后对每一位都进行“与”逻辑运算,并得到结果。
例:
5 & 11 = 1
解释:
5 = 0b0101
11 = 0b1011
0b0101 & 0b1011 = 0b0001
因为:第4位(最高位)0&1=0, 第3位1&0=1,第2位0&1=0,最低位1&1=1,因此结果为0b0001
写成竖式比较直观:
   5:  0 1 0 1
  11:  1 0 1 1
---------------
   1:  0 0 0 1

由于“按位与”这个中文翻译在句子中看起来怪怪的,因此之后会用英文bitwise And来表示这个运算。
由And运算的定义可知:无论是0还是1,只要跟0进行了And运算,结果都是0,只要跟1进行了And运算,结果都是它本身。所以Bitwise And运算可以帮助我们屏蔽掉不想要的数据。比如,我们读取到了一个8bit长度的数据,但是我们仅对其中的低4位感兴趣,那么我们就可以利用Bitwise And来屏蔽掉其高4位的数据:
0b???????? & 0b00001111 = 0b0000????
如果我们仅对高4位感兴趣,则:
0b???????? & 0b11110000 = 0b????0000
其中,0b00001111和0b11110000就可称为位掩码(Bit Mask),因为它屏蔽掉了我们不感兴趣的部分。Mask有“掩盖”、“遮罩”的含义,故bit mask译为“位掩码”。在实际操作中,位掩码通常会被写成十六进制。因此上述的两个位掩码分别为0x0F和0xF0。
我们知道,eud读取的数据一定是u32,所以针对它的位掩码一定也是32bit的,写成十六进制形式就是一个8位十六进制数。假设我们仅对u32中的高字节的u16感兴趣,则我们需要施加位掩码0b1111 1111 1111 1111 0000 0000 0000 0000,即0xFFFF0000。同理,如果我们仅对低字节的u16感兴趣,则需要施加位掩码0x0000FFFF。

比如,内存0xAABBCC00中储存了一个u16数据1234,即0x04D2,内存0xAABBCC02中储存了一个u16数据6789,即0x1A85,即:
0xAABBCC00: 0xD2
0xAABBCC01: 0x04
0xAABBCC02: 0x85
0xAABBCC03: 0x1A
我们用eud读取数据时只能在0xAABBCC00读取一个u32,读取到的数据为0x1A8504D2,即444925138=6789*65536+1234。 假设我们仅对高字节的u16感兴趣,即我们想要获取位于0xAABBCC02的u16,则我们可以对0xAABBCC00中的u32施加位掩码0xFFFF0000,因而得到0x1A850000,即444923904=6789*65536。注意,使用这个位掩码之后得到的数据是0x1A850000而不是0x1A85
假设我们仅对低字节的u16感兴趣,即0xAABBCC00的u16,则可使用位掩码0x0000FFFF,因而得到0x000004D2,即1234。

下面介绍scmd触发中的条件"EUD: Memory Value (Masked)"的含义:
下面这个condition:

p14

p14

等价于下面的TEP代码(需使用TEP v1.0或更高版本!)
MemoryX(0x0057F1D4, AtLeast, 8386808, 0xFFFF0000);
它的直接含义为:
对内存0x0057F1D4所储存的u32数据使用位掩码0xFFFF0000后所得到的数值小于等于8386808
注意到8386808=128*65536,所以这个condition的本质含义为:
0x0057F1D6所储存的u16数据的数值小于等于128
为了方便书写理解,下文将统一使用TEP代码的形式来表示"EUD: Memory Value (Masked)"条件。

再看上面的0xAABBCC00的例子,如果我们想要写出一个condition:“内存0xAABBCC02中的u16数据的数值恰好为6789”,则我们在scmd中要写:
MemoryX(0xAABBCC00, Exactly, 444923904, 0xFFFF0000);
如果我们想要写出一个condition:“内存0xAABBCC00中的u16数据的数值恰好为1234”,则我们在scmd中要写:
MemoryX(0xAABBCC00, Exactly, 1234, 0x0000FFFF);

同理,考虑如下condition:

p15

p15


Switch("Switch 11", Set);
如果将它写成EUD condition,要如何写?
首先我们知道,Switch Table位于0x58DC40,每个switch占用1bit,每8个switch占用1字节,因此"Switch 11"位于Switch Table的首4个字节中的第2个字节的第3位(我们仅对这1bit感兴趣,需要屏蔽其他31bit),因此我们需要读取的内存为0x58DC40,需要加的位掩码为:
0b0000 0000 0000 0000 0000 0100 0000 0000
即0x00000400
即写成EUD condition是:
MemoryX(0x0058DC40, Exactly, 1024, 0x00000400);

读者可自行试验:

p16

p16




回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-12-21 17:25:52 | 显示全部楼层
本帖最后由 PereC 于 2020-12-22 00:33 编辑

下面介绍scmd触发中的动作"EUD: Modify Memory (Masked)"的含义:
下面这个action

p17

p17

等价于TEP代码:
SetMemoryX(0x0058D2B4, Add, 16777216, 0xFF000000);
它的直接含义为:
用0x0058D2B4内的数据使用跟0xFF000000做bitwise And运算得到数值x
用16777216跟0xFF000000做bitwise And运算得到数值y
计算x+y的值,得到结果z,将z写成32位二进制数。然后看0xFF000000的哪些位是1,就将z的哪些位写入0x0058D2B4的对应bit。
它的本质含义为:
将0x0058D2B7内储存的u8数据增加1,即P1的Terran Infantry Weapons等级+1

比如0x0058D2B4储存的u32为0x04030201,则SetMemoryX(0x0058D2B4, Add, 16777216, 0xFF000000)之后,0x0058D2B4储存的u32变为0x05030201。计算过程为:
0x0058D2B4储存的u32是0x04030201,跟0xFF000000做bitwise And运算之后为0x04000000
16777216即为0x01000000,跟0xFF000000做bitwise And运算之后为0x01000000
计算两数之和,为0x05000000
由于0xFF000000只有第25至32位为1,所以仅修改0x04030201的第25至32位(即最高字节),修改为0x05000000的第25至32位,修改结果为0x05030201

当然,在实际操作中,我们不用思考这么复杂的事情,因为我们使用位掩码的目的就是屏蔽掉我们不感兴趣的bit,因此我们只需要知道SetMemoryX里面的位掩码的功能是“保持被屏蔽的bit不被修改、只修改我们感兴趣的bit”即可。上面的例子中,位掩码为0xFF000000,所以说明我们对0x0058D2B4储存的u32数据的低3字节都不感兴趣,希望让他们保持不变,并且只修改最高字节的数据。

思考:
如果我们想将0x0058D2B4内储存u8数据的数值增加1,那么下面两个Action是否都可达到我们的目的?
SetMemoryX(0x0058D2B4, Add, 1, 0x000000FF);
SetMemory(0x0058D2B4, Add, 1)
答:
第一个是正确的,第二个是错误的。注意,我们的目的是将0x0058D2B4内储存u8数据的数值增加1,而不是将0x0058D2B4内储存u32数据的数值增加1,所以我们必须保证0x0058D2B5、0x0058D2B6、0x0058D2B7内的数据保持不变。假设下面4个地址储存的数据为:
0x0058D2B4: 0xFF
0x0058D2B5: 0x00
0x0058D2B6: 0x00
0x0058D2B7: 0x00

那么第一个action的结果是:
0x0058D2B4: 0x00
0x0058D2B5: 0x00
0x0058D2B6: 0x00
0x0058D2B7: 0x00
因为根据u8的加法计算法则:0xFF + 1 = 0x00
注:这种现象叫做“溢出(overflow)”,即计算结果超出该数据所能表示的最大值,当数据类型为u8时,该数值将仅保留最低的8bit,即用结果去除以256,取余数。
u8加法:0xFF + 1 = 0x100(溢出),仅保留最低的8bit,即0x00
u8加法:0xFF + 0xFF = 0x1FE(溢出),仅保留最低的8bit,即0xFE
u32加法:0x000000FF + 0x000000FF = 0x000001FE,正常
u32加法:0xFFFFFFFF + 3 = 0x100000002(溢出),仅保留最低的32bit,即0x00000002

第二个action的结果是:
0x0058D2B4: 0x00
0x0058D2B5: 0x01
0x0058D2B6: 0x00
0x0058D2B7: 0x00
因为:0x000000FF + 1 = 0x00000100

很显然,第二个action不能保证“只修改最低的字节、保持3个高字节数据不变”,所以我们只能使用第一个action,即利用位掩码。

所以说,位掩码可以协助我们读取和修改u16、u8、Boolean等等各种类型的数据。


顺便科普一下0x0058D2B0(SC Upgrades Researched)的数据结构。这个内存中储存的是Upgrade Researched Table的SC部分,它的另一部分(BW部分)在0x0058F32C。这个table也是一个二维数组,详细介绍如下:

p18

p18

正如简介所写,这个table的行和列跟Death Table正好相反,本table的行索引为playerID,而列索引为UpgradeID,总共12行,46列。若将此二维数组命名为UpgradeResearchedTableSC,则UpgradeResearchedTableSC[3][5]表示的是playerID为3的玩家的UpgradeID为5的升级等级,即P4的Protoss Armor等级。DeathTable中的每个元素都是u32,而UpgradeResearchedTableSC中每个元素都是u8。

playerID \ Uprade ID (SC) 0
Terran Infantry Armor
1
Terran Vehicle Plating
2
Terran Ship Plating
3
Zerg Carapace
4
Zerg Flyer Caparace
...... 45
Unknown Upgrade45
0 (Player 1)
1
2
3
4
5
6
7
8
9
10 (Player 11)
11 (Player 12)

所以我们想要读取或修改其中的u8数据,就要使用位掩码。

习题:
1. 若0x58D5B0中储存的u32数据为0x04030201,则
(1)MemoryX(0x58D5B0, Exactly, 196608, 0x00FF0000)为True还是False?
(2)MemoryX(0x58D5B0, Exactly, 1, 0x000000FF)为True还是False?
(3)执行SetMemoryX(0x58D5B0, Add, 1, 0x000000FF)之后,0x58D5B0中储存的u32数据变为什么?
(4)执行SetMemoryX(0x58D5B0, SetTo, 1, 0x0000FF00)之后,0x58D5B0中储存的u32数据变为什么?
(5)执行SetMemoryX(0x58D5B0, SetTo, 256, 0x0000FF00)之后,0x58D5B0中储存的u32数据变为什么?
(6)若想将0x58D5B0中储存的u32的最高字节内储存的u8的值设为3,则对应的action为(使用SetMemoryX)?

2. 将下列文字转化为scmd触发:
触发拥有者:Player1
条件:Player3的Zerg Missile Attacks等级大于3
动作:将Player8的Zerg Flyer Caparace等级设定为20


回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|站点地图|永恒的星际联盟 ( 苏ICP备17020173号-1 )

GMT+8, 2021-1-18 05:29 , Processed in 0.021804 second(s), 7 queries , Gzip On, File On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表