查看“EUD”的源代码
←
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参数。[http://www.staredit.net/wiki/index.php/Scenario.chk#List_of_Players.2FGroup_IDs playerID的正常取值范围为0至26],若将Deaths/SetDeaths的playerID参数设为此范围之外的值(除去0至26的任何在-2147483648至2147483647之间的数值),则会被视为Extended Player Death,简称EPD。[http://www.staredit.net/wiki/index.php/Scenario.chk#Trigger_Unit_Types 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 === 星际重制版(SC:Remastered)修复了上文提到的Deaths/SetDeaths漏洞,因此在重制版刚刚发布时,已经不再有任何eud功能。但是暴雪的软件工程师[https://starcraft.fandom.com/wiki/Elias_Bachaalany Elias Bachaalany]在重制版发布不久后便开发出了“重制版EUD模拟器”,该功能伴随星际[https://news.blizzard.com/en-gb/starcraft/21313396/patch-1-21-0-the-return-of-eud-maps 1.23.0版本]于2017年12月一并发布。自此之后,星际重制版在遇到了含有EUD触发的地图时,会自动启用EUD模拟器来执行地图中的触发,这使得作者依然可以像以前一样通过Deaths/SetDeaths触发来在重制版实现EUD功能。 在星际重制版中,暴雪限制了地图触发对于内存的读写:有的内存只可读取、不可写入,有的内存不可读取或写入,只有少部分内存既可读取又可写入,详见内存表。如果EUD地图在游戏中运行某条触发时读取或写入了非法内存,则游戏立即被终止(弹窗报错:抱歉,这张EUD地图现在不被支持......错误码是一个十六进制数,用0xFFFFFFFF减去这个十六进制数得到的结果就是当前触发正在尝试读取或写入的非法内存地址)。这就导致重制版EUD技术在功能上有了诸多限制,比如重制版无法修改任何与模型、图像相关的东西,比如无法拓展单位上限,比如无法将1.08的EUD综合插件照搬到重制版等等。 在星际争霸重制版中,EUD地图拥有以下特点: (1)单位上限只能为原版的1700,而不是拓展的单位上限3400。(建立主机时,“单位上限”选项整个一行均为灰色,并被强制选择为“原版”,而无法选择“拓展”) (2)无法在游戏中保存游戏 (3)游戏结束后无法保存录像 韩国制图大神trigger king开发了名为eudplib的python程序库,搭建了重制版的EUD框架,其地位相当于1.08的EUD综合插件。此外,他还基于eudplib发明了[[EpScript|epScript编程语言]](简称eps语言)。重制版的eud制图工具(EE2和EE3)即是基于eps语言的GUI。这个eud框架极其强大,即便重制版的eud条件苛刻,作者们仍然可以基于这个框架制作出很多精彩的地图。 阅读以下教程前需要准备的工具为: (1) [[SCMD#.E6.9C.80.E6.96.B0.E7.89.88.E5.AE.98.E6.96.B9.E8.8B.B1.E6.96.87.E7.89.88.EF.BC.88.E6.8E.A8.E8.8D.90.E4.BD.BF.E7.94.A8.EF.BC.89|最新版的scmd2]] (2) scmd2插件[[TrigEditPlus|TrigEditPlus(简称TEP)最新版]] == 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就进一位。 {| class="wikitable" |+十进制,二进制,十六进制数字对应表 |十进制 |二进制 |十六进制 | |十进制 |二进制 |十六进制 | |十进制 |二进制 |十六进制 | |十进制 |二进制 |十六进制 |- |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 转化为二进制数的过程: {| class="wikitable" |+短除法 !除数 !被除数,商 !余数 |- |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位的十六进制数来表示一个字节所储存的数据: {| class="wikitable" |+用十六进制数表示一个字节所储存的数据 !字节的状态 !用二进制数表示 !用十六进制数表示 !用十进制数表示 |- |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。 即: {| class="wikitable" |+ !地址 !数据 |- |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种不同的信息,并且在这个例子中,对应关系如下: {| class="wikitable" |+此例中,数据与其实际含义的对应关系 !字节中储存的数据 !在此例中,它所代表的实际含义 |- |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之间的所有整数了: {| class="wikitable" |+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)即高字节优先: {| class="wikitable" |+大端序 !内存地址 !数据 |- |0x0000FFF0 |0x00 |- |0x0000FFF1 |0x7F |} 小端序(little endian)即低字节优先: {| class="wikitable" |+小端序 !内存地址 !数据 |- |0x0000FFF0 |0x7F |- |0x0000FFF1 |0x00 |} 读取数据时也是同理。假设目前内存中储存的数据如下: {| class="wikitable" !内存地址 !数据 |- |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中,得到的结果是: {| class="wikitable" |+ !内存地址 !数据 |- |0x0000FFF0 |0x4E |- |0x0000FFF1 |0x61 |- |0x0000FFF2 |0xBC |- |0x0000FFF3 |0x00 |} 地址0x0000FFF0储存的数据为0x4E 地址0x0000FFF1储存的数据为0x61 地址0x0000FFF2储存的数据为0xBC 地址0x0000FFF3储存的数据为0x00 解释:12345678表示为十六进制是0x00BC614E,按照低字节优先的方式储存。 将127以四字节非负整数(little endian)的形式储存到0x0000FFF0中,得到的结果是: {| class="wikitable" !内存地址 ! 数据 |- |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个整数。规则为: {| class="wikitable" |+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)'''规则。星际中储存的有符号整数皆使用该规则。 注意到,当s8数据所代表的数值为非负数时,它跟u8完全一样。这个特性适用于所有的“二补数”规则下的有符号整数数据类型。 双字节(s16)和三字节(s24)略。 =====四字节有符号整数(32-bit signed integer, 简称s32)===== 储存的范围是-2147483648至2147483647之间的所有整数,共2^32个整数。也是使用二补数规则,即为: {| class="wikitable" |+s32 !数据 !数据的意义 |- |0x00000000 |整数0 |- |0x00000001 |整数1 |- |... |... |- |0x000000FF |整数255 |- |0x00000100 |256 |- |... |... |- |0x7FFFFFFF |2147483647 |- |0x80000000 | -2147483648 |- |0x80000001 | -2147483647 |- |... |... |- |0xFFFFFFFF | -1 |} 例: 向0x0000FFF0写入一个s32(little endian)数值-2147483647,得到的结果是: {| class="wikitable" !内存地址 !数据 |- |0x0000FFF0 |0x01 |- |0x0000FFF1 |0x00 |- |0x0000FFF2 |0x00 |- |0x0000FFF3 |0x80 |}地址0x0000FFF0储存的数据为0x01 地址0x0000FFF1储存的数据为0x00 地址0x0000FFF2储存的数据为0x00 地址0x0000FFF3储存的数据为0x80 解释:根据s32规则,-2147483647对应的数据是0x80000001,按照低字节优先的方式储存,第一个字节储存0x01,中间两个字节储存0x00,第四个字节储存0x80。 例:内存状态如下时: {| class="wikitable" !内存地址 !数据 |- |0x0000FFF0 |0x01 |- |0x0000FFF1 |0x00 |- |0x0000FFF2 |0x00 |- |0x0000FFF3 |0x80 |}从地址0x0000FFF0读取一个s32(little endian),读取的结果是: 首先用little endian的规则来获取数据,获取到的数据为0x80000001,然后按照s32的规则,这个数据所代表的数是-2147483647,即为读取结果。 ==== 5. 定点数(Fixed-point) ==== 这个数据类型是用来储存小数的,但是其效果等价于整数。星际中只有极少数数据的类型是小数(其实本质上也是整数),比如单位的血量值(hitpoints)、实时护盾值(shield)都是是4字节有符号定点数(4 byte signed fixed-point)类型。 因此,关于定点数的知识,本教程仅介绍星际中单位的血量的方法。注:在计算机领域,浮点数(float-point)是使用率最高的含小数点数据类型,但星际的游戏数据没有用到浮点数。 星际用4字节储存单位的血量,其中3字节(高3字节)是血量的整数部分(有符号),1字节(最低字节)是血量的小数部分。其效果等价于4字节有符号整数(s32)。虽然星际游戏中在正常情况下不会出现负数血量,但星际仍使用有符号类型来储存血量。 规则如下: {| class="wikitable" !数据 !数据的意义(十六进制) !数据的意义(十进制) |- |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储存着地图中某个枪兵的血量。 假设其数据为: {| class="wikitable" !内存地址 !数据 |- |0x0059CCB0 |0x00 |- |0x0059CCB1 |0x28 |- |0x0059CCB2 |0x00 |- |0x0059CCB3 |0x00 |} 则其所代表的数值为40。解释:读取到的数值为0x000028.00,整数部分为0x000028,即为40;小数部分为0x00,即为0 假设数据为: {| class="wikitable" !内存地址 !数据 |- |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这种血量 如果我们以s32的方式来读取血量,那么读取到的数值除以256就是单位的实际血量。故这种数据类型等价于整数类型。 在实际操作中,我们会以u32来读取该数据。注意到,血量在绝大多数情况下不会小于0,所以用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,即: {| class="wikitable" !内存地址 !数据 |- |0x00628430 |0xA8 |- |0x00628431 |0xCC |- |0x00628432 |0x59 |- |0x00628433 |0x00 |}则我们称:内存0x00628430储存的这个指针变量指向内存地址0x0059CCA8 当指针储存的值为0时,我们称它为'''空指针(Null Pointer)'''。在eud制图的实际操作中,当我们需要使用指针时,必须先确保该指针不为Null,才可使用。 指针变量对数据的读写、内存的分配有着至关重要的作用,不过这不是本节内容的重点,故暂不做详细介绍。以后可能会在科普CUnitNode结构时做详细介绍。注:UnitNode Table (CUnit Table)储存着地图上1700个单位的详细信息,是一个双向链表,unit数据的读写皆由指针控制。 习题: 1. 在某个内存地址中写入新数据,是否会覆盖掉这个地址内的原有数据? 2. 如果我们在内存0x00AABBCC中以little endian的方式写入了一个四字节非负整数(u32),那么我们在读取这个信息时,应该在哪个内存读取?以什么规则读取? 3. 假设内存中储存着如下数据: {| class="wikitable" !内存地址 !数据 |- |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,储存它们(的开关状态)至少需要多少字节? === 【第四章】数组(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)就是一个典型的二维数组: {| class="wikitable" |+死亡数表 Death Table |UnitID\PlayerID !0 (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 Marine) |0 |0 |8 |0 |6 |0 |0 |1 |0 |0 |0 |0 |- !1 (Terran Ghost) |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |- !2 |0 |0 |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 |0 |0 |0 |0 |0 |0 |0 |0 |0 |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) 星际是使用C/C++代码编写的,因此二维数组在内存中的储存方式为:把每一行都当成一个普通数组来储存,用连续的内存来储存第一行,然后紧接着储存第二行,以此类推。一行一行地储存,而不是一列一列地储存。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这座大厦的理论基石。 {| class="wikitable" !内存地址 !数据类型 !数据的含义 |- |0x0058A364 |u32(4字节无符号整数,小端序) |P1的枪兵死亡数 |- |0x0058A368 |u32 |P2的枪兵死亡数 |- |0x0058A36C |u32 |P3的枪兵死亡数 |- |0x0058A370 |u32 |P4的枪兵死亡数 |- |... |... |... |- |0x0058A364 + 11 * 4 |u32 |P12的枪兵死亡数 |- |0x0058A364 + 12 * 4 |u32 |P1的Ghost死亡数 |- |0x0058A364 + 13 * 4 |u32 |P2的Ghost死亡数 |- |... |... |... |- |0x0058A364 + 2735 * 4 |u32 |P12的227号单位的死亡数 |}举例: 问: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所在地址的偏移量为(nx+y)s,其中x=0,1,2,...,m-1,y=0,1,2,...,n-1 再举一例: 每个玩家前46项升级是否可用(SC Upgrades Available (0-45)),就是一个46*12的二维数组,每个元素为u8(单字节整数),0代表本玩家本升级不可用,1代表可用,2至255没有意义。 注:除了SC Upgrades(共46项)之外,还有BW Upgrade(共15项),前者是星际1原版的升级,后者是星际1资料片所增加的升级项目。 {| class="wikitable" |+ SC Upgrades Available (0-45)表 |PlayerID\UpgradeID !0号升级 (Terran Infantry Armor) (人族小兵防御) !1号升级 (Terran Vehicle Plating) (人族车辆防御) !2 (Terran Ship Plating) (人族空军防御) !3 (Zerg Carapace) (虫族地面防御) !... !44 (Khaydarin Core (Arbiter +50)) (仲裁者+50能量上限) !45 (Unknown Upgrade45) |- !0 (Player1) |1 |1 |1 |1 |... | |0 |- !1 (Player2) |1 |1 |1 |1 |... | |0 |- !2 (Player3) |1 |1 |1 |1 |... | |0 |- !3 |1 |1 |1 |1 |... | |0 |- !4 |1 |1 |1 |1 |... | |0 |- !5 |1 |1 |1 |1 |... | |0 |- !6 |... | | | | | | |- !7 |... | | | | | | |- !8 |... | | | | | | |- !9 | | | | | | | |- !10 | | | | | | | |- ! 11 (Player12) | | | | | | | |} 此表有12行和46列(总共有12个玩家),所以这个数组是一个12×46的数组。并且,数组的元素是u8。此数组所在内存地址为 0x0058D088。与DeathTable不同,此表中,玩家ID变成了行。因此,“Player3的Zerg Carapace是否可用”这个信息在上表中的第3行、第4列,是储存0x0058D088 + 46 * 2 + 3这个内存中的u8数据,当其值为1时,代表P3可以升级Zerg Carapace,当其值为0时,代表P3不能升级Zerg Carapace,即其虫族升级建筑(Zerg Evolution Chamber)不含有该升级按钮。 {| class="wikitable" !内存地址 !数据类型 !数据的含义 |- |0x0058D088 |u8(单字节无符号整数) |P1的Terran Infantry Armor是否可用(1为可用,0位不可用) |- |0x0058D089 |u8 |P1的Terran Vehicle Plating是否可用 |- |0x0058D08A |u8 |P1的Terran Ship Plating是否可用 |- |0x0058D08B |u8 |P1的Zerg Carapace是否可用 |- |... |... |... |- |0x0058D088 + 45 |u8 |P1的45号升级是否可用 |- |0x0058D088 + 46 |u8 |P2的Terran Infantry Armor是否可用 |- |0x0058D088 + 46 + 1 |u8 |P2的Terran Vehicle Plating是否可用 |- |0x0058D088 + 46 + 2 |u8 |P2的Terran Ship Plating是否可用 |- |... |... |... |- |0x0058D088 + 46*2 |u8 |P3的Terran Infantry Armor是否可用 |- |0x0058D088 + 46*2 + 1 |u8 |P3的Terran Vehicle Plating是否可用 |- |... |... |... |- |0x0058D088 + 550 = 0x58D2AE |u8 |P12的44号升级是否可用 |- |0x0058D088 + 551 = 0x58D2AF |u8 |P12的45号升级是否可用 |}习题: (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表示)? === 【第五章】初识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所在地址的偏移量为(nx+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(...), 其TEP代码的语法为: Deaths condition: <source lang="lua"> Deaths(playerID, comparison, number, unitID) </source> 其中: [http://www.staredit.net/wiki/index.php/Scenario.chk#List_of_Players.2FGroup_IDs playerID]: 一个u32数据,合法(有意义的)取值范围为0至26。在TEP代码中,此参数可以写成P1,P2,P3,...,P8,...P12,...或CurrentPlayer等,或者任意数字。详见[http://www.staredit.net/wiki/index.php/Scenario.chk#List_of_Players.2FGroup_IDs List of Players/Group IDs] [http://www.staredit.net/wiki/index.php/Scenario.chk#Numeric_Comparisons comparison]: 一个u8数据,值为0 (代表AtLeast),或 1 (AtMost),或10 (Exactly)。在TEP代码中,此参数可以写成AtLeast或AtMost或Exactly或任意数字。详见[http://www.staredit.net/wiki/index.php/Scenario.chk#Complete_Modifier_List Complete Modifier List] number: 一个u32数据,任意取值都可以(0至4294967295)。 [http://www.staredit.net/wiki/index.php/Scenario.chk#Trigger_Unit_Types unitID]: 一个u16数据,合法取值为0至232。在TEP代码中,此参数可以写成"Terran Marine", "Terran Ghost"等任意单位名字符串,或者任意数字。详见[http://www.staredit.net/wiki/index.php/Scenario.chk#Trigger_Unit_Types Trigger Unit Types] 含义: Deaths(1, AtLeast, 3, 0)的含义为“P2的Terran Marine死亡数大于等于3”。注,写成Deaths(P2, 0, 3, "Terran Marine")也表示相同的意思。 SetDeaths action:<source lang="lua"> SetDeaths(playerID, modifier, number, unitID) </source>其中: playerID、number、unitID同上 [http://www.staredit.net/wiki/index.php/Scenario.chk#Number_Modifiers modifier]: 一个u8数据,值为7(SetTo),或 8 (Add),或9 (Subtract)。在TEP代码中,此参数可以写成SetTo或Add或Subtract或任意数字 含义: SetDeaths(1, Add, 3, 0)的含义为“将P2的Terran Marine死亡数增加3” 举例: [[文件:EUD-05-01 re.png]] 以上触发所对应的TEP代码为: <source lang="lua"> Trigger { players = {P1}, conditions = { Deaths(P2, Exactly, 3, "Terran SCV"); }, actions = { SetDeaths(P5, Add, 666, "Terran Vulture"); }, } </source> 如果把P1、P4和单位的名称改成PlayerID和UnitID的形式,并把comparison和modifier参数也改成数值形式,则可以写成: <source lang="lua"> Trigger { players = {0}, conditions = { Deaths(1, 10, 3, 7); }, actions = { SetDeaths(4, 8, 666, 2); }, } </source> 以上两种TEP代码写法是完全等价的,二者会被编译为完全相同的触发。其中,全部使用数值的写法更贴近触发的本质。无论你用何种工具(scmd自带的触发编辑器或者TEP代码等),触发都会以数据的形式存入地图文件(chk文件的TRIG section)中。具体来讲,每一个触发都会占用2400字节,触发中的每个condition占用20字节,每个action占用32字节,详见[http://www.staredit.net/wiki/index.php/Scenario.chk 星际地图chk结构]。 其中,Deaths(P2, Exactly, 3, "Terran SCV")这个condition在chk文件中储存的形式为: [[文件:EUD-05-02.png]] SetDeaths(P5, Add, 666, "Terran Vulture")这个action在chk文件中储存的形式为: [[文件:EUD-05-03.png]] 可见,我们使用编辑器来编辑触发的本质就是想地图的chk文件中注入一些数据。之后在游戏开始时,星际争霸程序会读取地图文件中的这些数据,并将其再翻译为触发,然后执行。理论上来讲,我们可以在地图文件中写入任何数据。比如我们知道,playerID这个参数的本质是一个u32数据,我们可以对其写入任何在0至4294967295之间的数值,虽然只有当其取值为0至26中的一个整数时才有实际意义,但是我们仍然可以任意设定它的数值。同理,虽然unitID的取值在0至232之间时才有实际意义,但是它的本质是一个u16数据,我们可以将其数值设为0至65535之间的任意数。 现在,我们尝试将Deaths的第一个参数(playerID)强行设置为100: [[文件:EUD-05-05.png]] 输入完毕后点击空白处,可以发现它变成了: [[文件:EUD-05-04.png]] 我们也可以使用如下TEP代码来得到等价的结果:<source lang="lua"> Trigger { players = {P1}, conditions = { Deaths(100, Exactly, 3, "Terran SCV"); }, actions = { SetDeaths(P5, Add, 666, "Terran Vulture"); }, } </source> 保存并编译后,再打开TEP界面,我可以看到:<source lang="lua"> Trigger { players = {P1}, conditions = { Memory(0x58A644, Exactly, 0x00000003); }, actions = { SetDeaths(P5, Add, 666, "Terran Vulture"); }, } </source> 之所以会变成这样,正是因为在允许index out of range的情况下,Deaths(100, Exactly, 3, "Terran SCV")含义为: playerID=100的玩家的unitID=7的单位的死亡数等于3 即: DeathTable[7][100]的值等于3 即: 内存地址0x0058A364 + 4(12*7 + 100) = 0x0058A644所储存的u32数据的数值等于3 其实,对任意给定的整数u和p,其中u为0至65535之前的任意整数,p为0至4294967295之间的除了13至26之外的任意整数(p取值12至26有特殊含义,之后会讲到), Deaths(p, Exactly, 3, u)的含义为: DeathTable[u][p]的值等于3 即内存地址0x0058A364 + 4(12u + p)所储存的u32数据的数值等于3 再举一个例子: SetDeaths(100, Add, 5, 1000)的含义为: 将DeathTable[1000][100]的u32数据的值加5 即:将地址为0x0058A364 + 4(12*1000 + 100) = 0x00596074内的数据的值+5 进一步思考,Deaths(playerID, comparison, number, unitID)这个condition的本质其实就是: 读取某个特定内存地址的u32数据,让这个数据与某个给定数值(number)进行比较。其中,这个特定的内存是由playerID和unitID这两个参数决定的。我们可以通过修改playerID和unitID的值,来让这个condition读取任意的内存。 同理,SetDeaths(playerID, modifier, number, unitID)这个action的本质是: 修改某个特定内存的u32数据。 将Deaths或SetDeaths的unitID参数设为232以上的数值,称作Extended Unit Death,简称EUD 将Deaths或SetDeaths的playerID参数设为26以上的数值,称作Extended Player Death,简称EPD 很显然,EPD和EUD的目的完全相同,都是为了读取或修改某个特定的内存地址内的数据,他俩也是完全等价的。我们通常管这种写触发的技术统称为EUD技术。 问:我们想要读取或修改内存地址0x59CCB0所储存的u32数据,应如何设定playerID和unitID? 答: 根据公式可得:0x0058A364 + 4(12u + p) = 0x59CCB0, 化简得:12u + p = 19027 所以,任意一组满足以上等式的u和p都可使我们触及(get access to)内存0x59CCB0。 比如,如果我们想要写一个condition,其功能为“获取0x59CCB0的u32数据,并判断其是否等于256”,则可以选择以下方式中的任意一种: Deaths(19027, Exactly, 256, 0) Deaths(19015, Exactly, 256, 1) Deaths(19003, Exactly, 256, 2) ...... Deaths(43, Exactly, 256, 1582) Deaths(31, Exactly, 256, 1583) (注意,playerID的取值为12至26有特殊含义,在此不做讨论) Deaths(7, Exactly, 256, 1585) Deaths(-5, Exactly, 256, 1586) Deaths(-17, Exactly, 256, 1587) ...... Deaths(-767381, Exactly, 256, 65534) Deaths(-767393, Exactly, 256, 65535) 注:参数playerID是一个4字节数据,在做加减乘除计算时,由于溢出效应(Overflow),将其视为s32或者u32是完全等价的,具体原因在此不做赘述。在上面的例子中,我们将其视为s32数据,方便理解。即,playerID的值为0xFFFFFFFF时,等价于它的值为-1。 虽然我们有6万多种方式来触及同一个内存地址,但是在实际运用时,我们通常将unitID取值为0,通过改变playerID的值来获取我们想要的内存地址。这是因为unitID只有2字节,而playerID有4字节。 令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的取值与内存的对应关系: {| class="wikitable" !内存地址 !playerID的取值(又称EPD值) |- |0x00000000 | -1452249 |- |0x00000004 | -1452248 |- |... |... |- |0x0058A35C | -2 |- |0x0058A360 | -1 |- |0x0058A364 |0 |- |0x0058A368 |1 |- |0x0058A36C |2 |- |... |... |- |0xFFFFFFFC |1072289574 |} 我们可以看到,每一个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章节中详细介绍。 继续以地址0x59CCB0举例: 如果我们想要读取0x59CCB0所储存的u32数值并判断这个值是否等于256,则一般我们会写如下TEP代码作为condition:<source lang="lua"> Deaths(19027, Exactly, 256, 0) </source> 如果我们想要向地址0x59CCB0写入u32数值256,则会写如下action:<source lang="lua"> SetDeaths(19027, SetTo, 256, 0) </source> 为了方便制图者,TEP为我们提供了Memory这个condition和SetMemory这个action。在这个例子中,我们可以直接写:<source lang="lua"> Memory(0x59CCB0, Exactly, 256) </source> 它完全等价于Deaths(19027, Exactly, 256, 0) 以及<source lang="lua"> SetMemory(0x59CCB0, SetTo, 256) </source> 它完全等价于SetDeaths(19027, SetTo, 256, 0) 了解了这些知识点后,内存表就变得很容易理解了。星际游戏中的大多数游戏数据都有固定的内存,比如单位死亡数这个二维数组永远储存在0x0058A364,玩家的实时水晶矿数量永远储存在0x0057F0F0等等等等。星际1的前辈们通过实践,将几乎所有的重要游戏数据所在的内存地址全部找到,并总结成一个列表,这就是星际1的内存表(Memory table),是eud制图的重要参考资料。 EUD Memory Table (原网址,需翻墙,作者Farty1billion) http://farty1billion.dyndns.org/EUDDB/ EUD Memory Table (Corbo克隆的数据库,不需翻墙,一直在更新,推荐使用) https://euddb.website/ EUD Memory Table (Ar3sgice大佬在克隆的数据库,已不再更新) https://ldconval.github.io/eudtools/Include/EUDDB.html Sayoka总结的内存表(已不再更新,需要翻墙) https://docs.google.com/spreadsheets/d/195jZK7Ap71eO1-qdVskC2xsVl7EbNdVp0hbh7N3D38A/edit#gid=0 打开内存表,我们可以看到Address、Player ID、Version等表头信息: [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(UnitNode Table), CSprite等等,将在之后的内容介绍。 下面举一例: 在内存表中找到地址0x58DC60,即Location Table,可以看到它的size为20,length为255,可知: 整个Location Table所占用的空间为20*255=5100字节,地图中总共有255个location,每个location的信息占用20字节。点击左边蓝色的0058DC60,即可查看详细说明。无法翻墙的读者可直接看以下截图: == 习题参考答案 == === 第一章习题 === (1) 1011是十进制,值为1011 0b1011是二进制, 值为11 0x1011是十六进制,值为4113 (2) 0xFF00 = 0b 1111 1111 0000 0000 0x00FF = 0b 0000 0000 1111 1111 (3) 0b 111 0111 1010 0101 = 0x77A5 (4)10^2-1=99, 16^4-1=65536, 2^8-1=255 === 第二章习题 === (1) 8bit (2) 2^32=4294967696 (3) 1字节 (4) 0xC3 (5) 0x59CD0C - 0x59CCA8 = 0x64 = 100 === 第三章习题 === 1. 是 2. 应该在0x00AABBCC中以little endian的方式读取u32 3. (1) 0x7F = 127 (2) 0x007F = 127 (3) 0x0080007F = 8388735 (4) 0x8000 = 32768 (5) 0x8000(s16) = -32768 (6) 0x0080 = 128 (7) 地址0x00FF8800: 0x80 地址0x00FF8801: 0x00 地址0x00FF8802: 0x80 地址0x00FF8803: 0x00 (8) 地址0x00FF8800: 0x80 地址0x00FF8801: 0x00 地址0x00FF8802: 0x00 地址0x00FF8803: 0x00 (9) 地址0x00FF8800: 0x7F 地址0x00FF8801: 0x00 地址0x00FF8802: 0x80 地址0x00FF8803: 0x80 (10) 0x0080007F 4. 读取到的数为0x20=32,little endian,后面3字节的数据都是0x00 5. 256/8=32字节 === 第四章习题 === (1)第一个数index为0,最后一个数index为59,总共占用120字节,第10个元素地址为0x00AA00+2*9=0x00AA12,index为20的元素所在地址为0x00AA00+2*20=0x00AA28 (2) 内存0x57F12C内的数据变为0x10 (3) a一共含有360个元素,总共占用1440字节,a[3][4]偏移量为(3*12+4)*4=160=0xA0 (4) 0x0058A364 + 4p 0x0058A364 + 4(12k + 2) === 第五章习题 === 1. Extended Unit Death, Extended Player Death, 等价 2. u32 (little endian) 3. 正确 4. EUD图的劣势: (1)无法使用单位拓展,即单位上限为原版的1700,而不是3400 (2)无法在游戏中保存游戏 (3)游戏结束后无法保存录像 5. 自然定义域为“所有4的倍数的内存地址”,即内存地址a的尾数为0或4或8或C 6. 只有(1)和(5)是合法的,其他3个都会报错。 解析:因为SetMemory仅允许输入4的倍数的内存地址,即内存地址尾数只能为0, 4, 8, C中的一个 7. EPD(0x58A364)=(0x58A364-0x58A364)/4 = 0 EPD值-2000所对应的内存地址为0x58A364-2000*4 = 0x588424 0x58DC60 + 20*63 = 0x58E14C 8. Trigger { players = {P1}, conditions = { Memory(0x0058A370, AtLeast, 3); }, actions = { SetMemory(0x0057F124, SetTo, 100); }, } 9. 游戏崩溃并报错0xFFFFFFFF === 第六章习题 === 1. (1) True (2) True (3) 0x04030202 (4) 0x04030001 (5) 0x04030101 (6) SetMemoryX(0x58D5B0, SetTo, 0x03000000, 0xFF000000)或SetMemoryX(0x58D5B0, SetTo, 50331648, 0xFF000000) 2. Trigger { players = {P1}, conditions = { MemoryX(0x0058D314, AtLeast, 0x04000000, 0xFF000000); }, actions = { SetMemoryX(0x0058D3F4, SetTo, 0x00140000, 0x00FF0000); }, } 或 Trigger { players = {P1}, conditions = { MemoryX(0x0058D314, AtLeast, 67108864, 0xFF000000); }, actions = { SetMemoryX(0x0058D3F4, SetTo, 1310720, 0x00FF0000); }, } 解析: Player3的Zerg Missile Attacks等级所在地址为0x0058D2B0+46*2+11 = 0x58D317,即0x58D314储存的u32的最高字节,故使用位掩码0xFF000000。大于3,即AtLeast 4,即AtLeast 4*16^6=67108864 Player8的Zerg Flyer Caparace等级所在地址为0x0058D2B0+46*7+4 = 0x58D3F6,即0x58D3F4储存的u32的次高字节,故使用位掩码0x00FF0000。设为20,即SetTo 20,即SetTo 20*16^4=1310720
返回至“
EUD
”。
导航菜单
个人工具
登录
名字空间
页面
讨论
变体
视图
阅读
查看源代码
查看历史
更多
搜索
导航
首页
最近更改
分类目录
常见问题
工作笔记
制图工具
所有页面
工具
链入页面
相关更改
特殊页面
页面信息