EUD
简介
概览
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 转化为二进制数的过程:
from datetime import datetime, timedelta t = '2021-09-19 01:55:19' ts = 1619269179