“EUD”的版本间的差异

来自星际争霸重制版地图研究所
跳到导航 跳到搜索
第1,128行: 第1,128行:


====8. 指针(Pointer)====
====8. 指针(Pointer)====
指针(指针变量)的读写标准等同于u32,也是一个四字节的非负整数,但是其储存的数据含义为内存地址。比如内存0xAABBCC00储存着一个指针变量,其值为0x0059CCA8,即:
指针(指针变量)的读写标准等同于u32,也是一个四字节的非负整数,但是其储存的数据含义为内存地址。比如内存0x00628430储存着一个指针变量,其值为0x0059CCA8,即:
{| class="wikitable"
{| class="wikitable"
!内存地址
!内存地址
!数据
!数据
|-
|-
|0xAABBCC00
|0x00628430
|0xA8
|0xA8
|-
|-
|0xAABBCC01
|0x00628431
|0xCC
|0xCC
|-
|-
|0xAABBCC02
|0x00628432
|0x59
|0x59
|-
|-
|0xAABBCC03
|0x00628433
|0x00
|0x00
|}则我们称:内存0xAABBCC00储存的这个指针变量指向内存地址0x0059CCA8
|}则我们称:内存0x00628430储存的这个指针变量指向内存地址0x0059CCA8


当指针储存的值为0时,我们称它为空指针(Null Pointer)。
当指针储存的值为0时,我们称它为空指针(Null Pointer)。


指针变量对数据的读写、内存的分配有着至关重要的作用,不过这不是本节内容的重点,故暂不做详细介绍。以后可能会在科普UnitNode结构时做详细介绍。注:UnitNode Table储存着地图上1700个单位的详细信息,是一个双向链表,unit数据的读写皆由指针控制。
指针变量对数据的读写、内存的分配有着至关重要的作用,不过这不是本节内容的重点,故暂不做详细介绍。以后可能会在科普CUnitNode结构时做详细介绍。注:UnitNode Table (CUnit Table)储存着地图上1700个单位的详细信息,是一个双向链表,unit数据的读写皆由指针控制。


习题:
习题:

2021年11月26日 (五) 16:53的版本

简介

概览

EUD是Extended Unit Death的缩写,是指星际争霸1地图编辑的一项技术,它的基本原理是将星际地图触发中的Deaths条件/SetDeaths动作中的playerID、unitID参数填写成超出其正常范围的数值来读写更大范围的内存,从而实现普通地图所无法拥有的更多功能。

在星际1.08时代,地图作者发现了星际地图触发系统中Deaths条件(Deaths(playerID, comparison, amount, unitID)))和SetDeath动作(SetDeaths(playerID, modifier, amount, unitID))的漏洞:其中的playerID参数和unitID参数可以填写超出其正常范围内的值,并且触发仍然会生效。这就使得Deaths条件可以读取更大范围的内存地址、SetDeaths动作可以修改更大范围的内存地址内的数据。星际地图的触发使用4字节来储存Deaths/SetDeaths的playerID参数,使用2字节来储存unitID参数。playerID的正常取值范围为0至26,若将Deaths/SetDeaths的playerID参数设为此范围之外的值(除去0至26的任何在-2147483648至2147483647之间的数值),则会被视为Extended Player Death,简称EPD。unitID的正常取值范围为0至232,若将Deaths/SetDeaths的unitID参数设为此范围之外的值(除去0至232的任何在0至65535之间的数值),则会被视为Extended Unit Death,简称EUD。在实际操作中,为了读写更大范围的内存,我们通常会使用EPD,或者EUD、EPD二者混用,即可在理论上读写所有内存地址(0x00000000至0xFFFFFFFF)。由于习惯问题,这项技术的名字还是以EUD命名,而非EPD。

含有至少一条使用了EUD或EPD的Deaths条件或SetDeaths动作的触发,被称为EUD触发。

含有至少一条EUD触发的地图被称为EUD地图。

星际1.08的EUD

星际1.08允许EUD触发读取、修改任意内存,因此1.08的EUD自由度很高,在理论上可以实现任何事情。各路大神开发了1.08的EUD综合插件(基本原理是通过修改内存直接改变星际触发中Comment动作的运行函数),并将其应用在制图工具Starcraft Map Cracker (SMC)内部。因此星际1.08的EUD地图达到了功能性上的巅峰,可以实现的功能包括但不限于:消除单位个数上限(原本的单位上限为1700个单位,1.08的EUD技术可将其拓展至无限)、修改单位的各种数据(武器、射程、子弹等)、修改单位的模型及图像、实现基于鼠标点击的按钮菜单系统及背包系统等等。

星际重制版的EUD

在星际重制版中,暴雪限制了地图触发对于内存的读写:有的内存只可读取、不可写入,有的内存不可读取或写入,只有少部分内存既可读取又可写入,详见内存表。如果在EUD地图在游戏中运行某条触发时读取或写入了非法内存,则游戏立即被终止(弹窗报错:抱歉,这张EUD地图现在不被支持......错误码是一个十六进制数,用0xFFFFFFFF减去这个十六进制数得到的结果就是当前触发正在尝试读取或写入的非法内存地址)。这就导致重制版EUD技术在功能上有了诸多限制,比如重制版无法修改任何与模型、图像相关的东西,比如无法拓展单位上限,比如无法将1.08的EUD综合插件照搬到重制版等等。

在星际争霸重制版中,EUD地图拥有以下特点:

(1)单位上限只能为原版的1700,而不是拓展的单位上限3400。(建立主机时,“单位上限”选项整个一行均为灰色,并被强制选择为“原版”,而无法选择“拓展”)

(2)无法在游戏中保存游戏

(3)游戏结束后无法保存录像


韩国制图大神trigger king开发了名为eudplib的python程序库,搭建了重制版的EUD框架,其地位相当于1.08的EUD综合插件。此外,他还基于eudplib发明了epScript编程语言(简称eps语言)。重制版的eud制图工具(EE2和EE3)即是基于eps语言的GUI。这个eud框架极其强大,即便重制版的eud条件苛刻,作者们仍然可以基于这个框架制作出很多精彩的地图。

EUD基础理论知识

【第一章】二进制数

人类使用十进制数(Decimal number)来记录数值的大小,十进制数的特点是:每个数由若干位组成,从小到大分别是个位、十位、百位...,每位上的数字都是0至9这10个数字中的一个。数数的时候,从0开始数,0,1,2,......,数到9的时候,下一个数要进一位,因此下一个数就是10(我们可以把它读作“一零”,方便理解)。

而计算机使用二进制数(Binary number)来记录数值的大小,特点是:每个数由若干位组成,每位上的数字只能是0或1中的一个。用二进制数来数数时,从0开始数,下一个数是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就进一位。

十进制,二进制,十六进制数字对应表
十进制 二进制 十六进制 十进制 二进制 十六进制 十进制 二进制 十六进制 十进制 二进制 十六进制
0 0 0 16 10000 10 32 100000 20 48 110000 30
1 1 1 17 10001 11 33 100001 21 49 110001 31
2 10 2 18 10010 12 34 100010 22 50 110010 32
3 11 3 19 10011 13 35 100011 23 51 110011 33
4 100 4 20 10100 14 36 100100 24 52 110100 34
5 101 5 21 10101 15 37 100101 25 53 110101 35
6 110 6 22 10110 16 38 100110 26 54 110110 36
7 111 7 23 10111 17 39 100111 27 55 110111 37
8 1000 8 24 11000 18 40 101000 28 56 111000 38
9 1001 9 25 11001 19 41 101001 29 57 111001 39
10 1010 A 26 11010 1A 42 101010 2A 58 111010 3A
11 1011 B 27 11011 1B 43 101011 2B 59 111011 3B
12 1100 C 28 11100 1C 44 101100 2C 60 111100 3C
13 1101 D 29 11101 1D 45 101101 2D 61 111101 3D
14 1110 E 30 11110 1E 46 101110 2E 62 111110 3E
15 1111 F 31 11111 1F 47 101111 2F 63 111111 3F


日常生活中,我们写的所有数都会被默认为是十进制数,比如你写了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 转化为二进制数的过程:

短除法
除数 被除数,商 余数
2 10
2 5 0
2 2 1
2 1 0
0 1

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

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

下面来讲二进制数和十六进制数的相互转化:

由于16正好等于2的4次方,所以二进制数和十六进制数的转化非常容易,完全不需要转化为十进制数然后再短除。

二进制数转化为十六进制数时,只需要将二进制数分为若干组即可,从最低位开始,每4位为一组,然后参照上面表格的第一列,将每组数替换为十六进制数字即可。

比如:

0b1110110101 = 0b 11 1011 0101 = 0x3B5

同理,十六进制数转化为二进制数,只需要将十六进制数的每一位上面的数字转化为其对应的二进制数即可。

比如0x70AB:

0x7 = 0b0111

0x0 = 0b0000

0xA = 0b1010

0xB = 0b1011

所以0x70AB = 0b0111000010101011

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

window自带的计算器有一个programmer模式(程序员模式),即可完成二进制、八进制、十进制、十六进制的互相转化及运算。


习题:

(1)

1011, 0b1011, 0x1011分别是多少进制的数字?分别求出他们所表示的数值(计算的结果用十进制数表示)

(2)将0xFF00和0x00FF转化为二进制数

(3)将0b111011110100101转化为十六进制数

(4)一个2位的十进制数所能表示的最大数值为多少?一个4位的十六进制数所能表示的最大数值为多少?一个8位的二进制数所能表示的最大数值为多少?


【第二章】 认识内存和内存地址

计算机储存数据使用的最小单元称为“位(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

...

计算机利用这些“位(bit)”的各种状态的组合来储存数据。

我们将8位作为一组,称为一个“字节(byte)”,也可简称为大写字母B,一个字节可以有256种不同的状态:

00000000

00000001

00000010

00000011

......

00001111

00010000

......

11111110

11111111

注意,11111110表示的是该字节的状态,也可以说它是该字节所储存的数据。可以发现,它与二进制数具有等价的形式。一个字节的每个状态刚与一个8位的二进制数一一对应,所以我们可以用8位二进制数来表示每个字节的状态。而一个8位的二进制数又刚好可以转化为一个2位的十六进制数。由于二进制数很冗长,所以我们一般使用一个2位的十六进制数来表示一个字节所储存的数据:

用十六进制数表示一个字节所储存的数据
字节的状态 用二进制数表示 用十六进制数表示 用十进制数表示
00000000 0b00000000 0x00 0
00000001 0b00000001 0x01 1
00000010 0b00000010 0x02 2
00000011 0b00000011 0x03 3
...... ...... ......
00001111 0b00001111 0x0F 15
00010000 0b00010000 0x10 16
...... ...... ......
11111110 0b11111110 0xFE 254
11111111 0b11111111 0xFF 255

注意!这个十六进制数只是表示了这个字节所储存的数据而已,而这个数据可以被解释为各种含义,比如可以被解释为一个数值,或者一个字符,等等等等。

在玩游戏的时候,所有的游戏数据都储存在内存中。为了更方便地存储和读取数据,内存把它的每一份储存空间(每个字节)都进行了编号,就好比每个住户都有一个门牌号。编号是从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内存表中的所有地址,以及本文之后的内容中所提到的所有的星际争霸游戏内的内存地址,其实都是相对的,并且我们对绝对的地址不感兴趣。

下例中的第一张图,每个字节所在地址是用十六进制表示的,第二张图的地址则是用十进制表示的。两张图的每个字节的数据都是用十六进制表示的。

EUD-02-01.png


EUD-01-02.png

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

即:

地址 数据
0x00000000 0x4D
0x00000001 0x50
0x00000002 0x51
0x00000003 0x1A
... ...
0x00000010 0x21
0x00000011 0x7B
... ...

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

习题:

(1) 一个byte等于多少bit?

(2) 4字节的储存空间总共有多少种可能的状态?

(3) 每个内存地址所对应储存空间有多大?

(4) 本节的示例图片中,地址0x0000017A所储存的数据是什么?

(5) 以0x59CCA8为基址,则0x59CD0C的偏移量为多少?

【第三章】数据的读取与写入

一个字节可以有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

...

0xFE对应的是1

0xFF对应的是0


当然,你还可以制定如下规则:

0x00对应0

0x01对应1

0x02对应2

...

0x7F对应127

0x80对应-128

0x81对应-127

...

0xFE对应-2

0xFF对应-1


甚至你还可以让一个字节去储存256个不同的偶数。

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


内存中的数据读取与写入的两大要素:

(1) 所要读取或写入的数据的地址

(2) 读取或写入数据的规则:包

1. 单字节非负整数(8-bit unsigned integer, 简称uint8或者u8)

这种规则可以让一个字节储存0至255之间的所有整数,具体读写规则见上面的第一个例子,在此不再赘述。

假设我们要往地址0x0000FF据时,无论这个地址内原本的数据是什么,我们都会将新数据直接写入,覆盖掉原有数据。

同理,如果我们要在地址0x0000FFF0读取一个uint8,假设0x0000FFF0地址里储存的数据为0x7F,那么我们读取的结果就是127

注:在计算机领域内,非负整数的学名是unsigned integer,应当直译为无符号整数,我在此使用“非负整数”方便大家理解。

2. 双字节非负整数(16-bit unsigned integer, 简称uint16或u16)

用两个字节来储存一个非负整数,显然我们就可以储存0至65535之间的所有整数了:

u16
数据 数据的意义
0x0000 整数0
0x0001 整数1
... ...
0x00FF 整数255
0x0100 256
0x0101 257
... ...
0xFFFF 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

读取数据时也是同理。假设目前内存中储存的数据如下:

读取到的结果就是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

地址0x0000FFF0储存的数据为0x4E

地址0x0000FFF1储存的数据为0x61

地址0x0000FFF2储存的数据为0xBC

地址0x0000FFF3储存的数据为0x00

解释:12345678表示为十六进制是0x00BC614E,按照低字节优先的方式储存。

将127以四字节非负整数(little endian)的形式储存到0x0000FFF0中,得到的结果是:

内存地址 数据
0x0000FFF0 0x7F
0x0000FFF1 0x00
0x0000FFF2 0x00
0x0000FFF3 0x00

地址0x0000FFF0储存的数据为0x7F

地址0x0000FFF1储存的数据为0x00

地址0x0000FFF2储存的数据为0x00

地址0x0000FFF3储存的数据为0x00

解释:127表示为十六进制是0x0000007F,按照低字节优先的方式储存,即第一个字节储存0x7F,后面3个字节都储存0x00。

在这个例子中我们可以看到,使用little endian的好处:在读取0x0000FFF0中的数据时,无论你读取的是单字节整数、双字节整数还是四字节整数,你读取到的都是127这个数。

注意:星际争霸1中所有整数型数据的储存方式皆为小端序(little endian),低字节优先!故下文全部默认是little endian

4. 有符号整数(signed integer)

单字节有符号整数(8-bit signed integer, 简称s8)

储存的范围是-128至127之间的所有整数,共256个整数。规则为:

s8
数据 二进制形式的数据 数据的意义
0x00 0b 0000 0000 整数0
0x01 0b 0000 0001 整数1
... ...
0x0F 0b 0000 1111 15
0x10 0b 0001 0000 16
0x11 0b 0001 0001 17
... ...
0x7E 0b 0111 1110 126
0x7F 0b 0111 1111 127
0x80 0b 1000 0000 -128
0x81 0b 1000 0001 -127
... ...
0xFE 0b1111 1110 -2
0xFF 0b1111 1111 -1

可以发现,在此规则中,数据最高位为1时,所代表的的数值就是负数,且任何正数x的相反数-x即为x的二进制码的补码+1,比如整数1所对应的数据的二进制形式为00000001,其补码为11111110,再加1就是11111111,即为-1;再比如比如整数127所对应的数据的二进制形式为01111111,其补码为10000000,再加1就是10000001,即为-127。这种方式被称为二补数(two's complement)规则。星际中储存的有符号整数皆使用该规则。

双字节(s16)和三字节(s24)略。

四字节有符号整数(32-bit signed integer, 简称s32)

储存的范围是-2147483648至2147483647之间的所有整数,共2^32个整数。也是使用二补数规则,即为:

s32
数据 数据的意义
0x00000000 整数0
0x00000001 整数1
... ...
0x000000FF 整数255
0x00000100 256
... ...
0x7FFFFFFF 2147483647
0x80000000 -2147483648
... ...
0xFFFFFFFF -1

例:

向0x0000FFF0写入一个s32(little endian)数值-2147483647,得到的结果是:

内存地址 数据
0x0000FFF0 0x01
0x0000FFF1 0x00
0x0000FFF2 0x00
0x0000FFF3 0x80

地址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. 定点数(Fixed-point)

这个数据类型是用来储存小数的。星际中只有单位的血量这一个数据是小数,且是4字节有符号定点数(4 byte signed fixed-point)类型,其他所有游戏数据皆为整数。

因此,关于定点数的知识,本教程仅介绍星际中单位的血量的方法。注:在计算机领域,浮点数(float-point)是使用率最高的含小数点数据类型,但星际的游戏数据没有用到浮点数。

星际用4字节储存单位的血量,其中3字节(高3字节)是血量的整数部分,1字节(最低字节)是血量的小数部分。

规则如下:

数据 数据的意义(十六进制) 数据的意义(十进制)
0x00000000 0x000000.00 0
0x00000001 0x000000.01 1/256
... ... ...
0x00000080 0x000000.80 0.5
... ... ...
0x000000FF 0x000000.FF 255/256
0x00000100 0x000001.00 1
... ... ...
0x7FFFFF00 0x7FFFFF.00 8388607
0x7FFFFF01 0x7FFFFF.01 8388607 又 1/256
... ... ...
0x7FFFFFFF 0x7FFFFF.FF 8388607 又 255/256
0x80000000 -0x800000.00 -8388608
0x80000001 -0x7FFFFF.FF -(8388607 又 255/256)
... ... ...
0xFFFFFFFF -0x000000.01 -1/256

例:

假如内存地址0x0059CCB0储存着地图中某个枪兵的血量。

假设其数据为:

内存地址 数据
0x0059CCB0 0x00
0x0059CCB1 0x28
0x0059CCB2 0x00
0x0059CCB3 0x00

则其所代表的数值为40。解释:读取到的数值为0x000028.00,整数部分为0x000028,即为40;小数部分为0x00,即为0

假设数据为:

内存地址 数据
0x0059CCB0 0x80
0x0059CCB1 0x27
0x0059CCB2 0x00
0x0059CCB3 0x00

则其表示的数值为0x000027.80,即0x27 + 0x80/256 = 39+128/256 = 39.5,即此枪兵的血量为39.5

注意,血量的小数部分为1字节,所以其精度为1/256,所以血量的小数部分只能为0, 1/256, 2/256, 3/256, ..., 255/256这256种情况中的其中一种,不可能出现0.3这种血量

如果我们以u32的方式来读取血量,那么读取到的数值除以256就是单位的实际血量。

6. 布尔变量(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,故在此暂不介绍其读写方式。

7. 字符(character)和字符串(string)

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

8. 指针(Pointer)

指针(指针变量)的读写标准等同于u32,也是一个四字节的非负整数,但是其储存的数据含义为内存地址。比如内存0x00628430储存着一个指针变量,其值为0x0059CCA8,即:

内存地址 数据
0x00628430 0xA8
0x00628431 0xCC
0x00628432 0x59
0x00628433 0x00

则我们称:内存0x00628430储存的这个指针变量指向内存地址0x0059CCA8

当指针储存的值为0时,我们称它为空指针(Null Pointer)。

指针变量对数据的读写、内存的分配有着至关重要的作用,不过这不是本节内容的重点,故暂不做详细介绍。以后可能会在科普CUnitNode结构时做详细介绍。注:UnitNode Table (CUnit 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,储存它们(的开关状态)至少需要多少字节?