EUD

来自星际争霸重制版地图研究所
Pere讨论 | 贡献2021年11月25日 (四) 19:44的版本
跳到导航 跳到搜索

简介

概览

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内存表中的所有地址,以及本文之后的内容中所提到的所有的星际争霸游戏内的内存地址,其实都是相对的,并且我们对绝对的地址不感兴趣。

让我们来看下面这个例子。下面第一张图中,每个字节所在地址是用十六进制表示的,第二张图的地址则是用十进制表示的。