Pere工作笔记
EUD:
Plugin may define 3 functions.
- onPluginStart : Run once before map's first trigger loop, before beforeTriggerExec.
- beforeTriggerExec : Run before every trigger loop
- afterTriggerExec : Run after every trigger loop
如果在main.edd中先后写了3行plugin:[a.eps], [b.eps], [c.eps]
则运行顺序为:
a.on(), b.on(), c.on(),
a.be(), b.be(), c.be(), SCMDtriggers, c.af(), b.af(), a.af(),
a.be(), b.be(), c.be(), SCMDtriggers, c.af(), b.af(), a.af(),...
ee2编辑器相当于代码的一个GUI,里面的triggerEditor属于一个plugin,里面的classic triggers全部在beforeTriggerExec里面,所以每一轮扫触发都是先扫完EUDtriggerEditor里面的触发,再扫scmd里面写的触发。
euddraft在工程文件(如main.edd)中写的plugin文件只认eps后缀和py后缀,如果[]内的路径有拓展名且是.py或.eps,则按照[]内的路径来读取文件,支持绝对路径(带磁盘符的路径)和相对路径(不带磁盘符,在当前工程文件路径内读取)。如果是拓展名不是py或eps或者不含拓展名,假设是[path\a.ext]则它会读取"euddraft.exe所在路径\plugin\path\a.ext.py",若这个路径不存在的话当然就会报错。所以在写main文件代码的时候,只要你想偷懒不加后缀名写[abc],就要保证euddraft的plugin文件夹里有abc.py文件。
euddraft在编译的时候,首先要把eps文件编译成py文件放在__epspy__文件夹中,然后再进一步编译py文件。
对于eudEditor来讲,它的真实工程文件是"EUDeditor.exe所在目录\Data\eudplibdata\"中的EUDEditor.eds。所以想让EE使用用户自己写的py或eps plugin的话,一定要在EE的plugin里的additional eds text里面注明完整的路径,比如[D:\sc\abc.py]或[D:\sc\abc.eps]。注意,磁盘后面的冒号可能要escape,变成\:,不太确定要不要这样。另外注意,如果plugin是eps格式的,那么它一定要放在工程文件所在的磁盘中,工程文件在D盘那么eps文件也需要在D盘,否则会报错,原因未知。
如果用py来写plugin,则所有的函数名都要加f_前缀,比如f_EPD
EUDdraft functions: (tct代指tempcustomText.py插件)
EPD(ptr): return地址ptr所对应的PID,例EPD(0x0059CCA9)==19025
bread(ptr): 在地址ptr处读取1字节,ptr为内存地址,例bread(0x0059CCA8)
wread(ptr): 在地址ptr处读取2字节
dwread(ptr): 在地址ptr处读取4字节,基本等价于dwread_epd(EPD(ptr))
dwread_epd(PID)或dwread_epd_safe(PID): 在地址PID处读取4字节,PID为地址所对应的playerID,例dwread_epd(19025)
epdread_epd(PID)或者epdread_epd_safe(PID): 如果PID处储存的内容是地址(ptr),则return这个地址所对应的PID,这个函数等价于EPD(dwread_epd(PID))
rand(): return a random u16
dwrand(): return a random u32
getseed(): 获取EUD内的_seed的值,注:默认情况下,此值在游戏开始时为0
srand(x): 将EUD内的_seed值设为x
randomize(): 将EUD的_seed设为星际内的seed值
eps有个奇怪的设定:
对于普通变量来讲,const是不可修改的,而var是可修改的
而对于array来讲,var是不可修改的,const反倒是可修改的。 (存疑)
例:
const a = EUDArray(3); \\声明
const b = [1,2]; \\声明
a[2]=1; \\没问题
b[0]=3; \\没问题
var c = [1,2,3]; \\声明
c[2]=1; \\报错
const可以储存string类型的变量,而var不可以。const还可以被赋值成各种类型的object,而var则不行。
euddraft里面的function可以return多个指,比如return 1,2; 相当于return了一个EUDArray。但是却不能以array作为argument。
Unit在内存中的结构为double-linked list,其中,内存地址0x006283F8储存的Last Unit Pointer是指内存地址最靠后的unit,而这个unit不一定在unit链表的末尾。0x00628430储存的First Unit Pointer是指内存地址最靠前的unit,同时也是unit链表的开头。其中,链表开头的unit的Previous Unit为0,链表结尾的unit的Next Unit为0。所以loop unit应该用First Unit Pointer作为开始,以NextUnit为0时作为结尾,整个过程不应该牵扯到Last Unit Pointer。
有一个关键变量叫做current player,简称cp,默认0,它可以看做是一个global variable,可以用于所有含有"current player"的触发。比如所有RunAIScript(Script)都会对当前的cp生效,所以在用RunAIScript之前,要设定好cp值。再比如tct.print()仅会在当前玩家的屏幕去print,所以在使用tct.print之前也要检查cp值。
getcurpl(): 返回当前的cp值
setcurpl(int): 将cp值设为int
getuserplayerid(): 适用于单人游戏,返回玩家所对应的playerID,通常使用setcurpl(getuserplayerid())然后用print来给玩家显示信息
下面的函数是一个很棒的函数:
EUDPlayerLoop()();
...
EUDEndPlayerLoop();
它会把cp值依次设为每一个active player,从当前cp值开始。运行完毕后的cp值就是loop开始前的cp值。
还有一个loop是EUDLoopPlayer(),loop的是每一个Human Player。当无Computer player时,如下使用即可等价于EUDPlayerLoop
foreach(p: EUDLoopPlayer()) {
setcurpl(p);
...
}
关于alliance table
SetAllianceStatus(p, status)的效果是把alliance table的坐标(curpl, p)位置的数设成status
例子:此时curpl = 3, 那么SetAllianceStatus(0, 2)就是把(3, 0)位置设为2(alllied victory)
当然实际情况很复杂,因为当alliance table的同一行不能同时存在1和2(除非是电脑玩家),所以在设置SetAllianceStatus(p, 2)或SetAllianceStatus(p, 1)时,(curpl, p)位置的数会被设为(curpl, curpl)
星际中有关string的东西大多会有一个dictionary(字典)的数据结构。字典由(key, value) pairs构成,每个pair记作 [key]: [value]。下面列举几个字典及其相关函数:
1. stat_txt.tbl dictionary ([TBLkey]: [TBLindex])
该字典包含了游戏中绝大多数预设string,默认版总共有1539项。具体内容为:
"Terran Marine": 1
"Terran Ghost": 2
"Terran Vulture": 3
"Terran Goliath": 4
"Goliath Turret": 5,
"Terran Siege Tank (Tank Mode)": 6,
"Siege Tank Turret (Tank Mode)": 7,
...
"AI Nuke Here": 1537,
"AI Harass Here": 1538,
"Set Unit Order To: Junk Yard Dog": 1539,
二模蛤的网站上还写了其他项:
"View Players": 1540,
...
"Replay Progress": 1545
"Paused": 1546
"Unlimited": 1547
与之相关的函数为:
$B("TBLkey")或EncodeTBL("TBLkey"): 输入一个key,输出TBLindex。比如$B("Terran Goliath")会得到4
GetTBLAddr(int)或GetTBLAddr("TBLkey"):
得到储存该字符串的地址(位于stat_txt.tbl string section内)。比如GetTBLAddr(1)或者GetTBLAddr("Terran Marine")就会得到"Terran Marine"对应的字符串的地址
const targetAddr = GetTBLAddr(TBLID);与以下eps代码等价:
const TBLaddr = dwread_epd(EPD(0x6D1238));
const targetAddr = TBLaddr + wread(TBLaddr + TBLID * 2);
注意,"TBLkey"必须是上面1547个之中的一个,输入其他字符串就会报错。这个与Map string不同。
且如果输入int,则int必须在1至1539的范围内,否则将out of range。
注意,上面的TBLkey仅仅是作为key而已,而"TBLkey"这个字符串本身并不一定在内存中实际存在。比如,内存中并不存在"Terran Siege Tank (Tank Mode)"这个字符串。"Terran Siege Tank (Tank Mode)"所对应的地址内的字符串(位于stat_txt.tbl string section内)打印出来是"Terran Siege Tank"
2. Map string dictionary ([MapStrKey]: [MapStrIndex])
该字典包含了触发中的string、自定义的Unit name、location名、自定义的switch名、地图名、任务简报等。在用euddraft编译时(armoha在euddraft-[0.8.6.8]-2019-05-06删掉了location名与switch名),location名和switch名会在字典中被删除。总数不定。具体内容可能为:
"Untitled Scenario": 1
"Destroy all enemy buildings.": 2
"Force 1": 4
"Force 2": 5
"Force 3": 6
"Force 4": 7
"custom unit name 1": 8
与之相关的函数:
$T("MapStrKey")或EncodeString("MapStrKey")或GetStringIndex("MapStrKey"): 原理同上
GetMapStringAddr(int)或tct.strptr(int)
或GetMapStringAddr("MapStrKey")或tct.strptr("MapStrKey"):
得到储存该字符串的地址(位于Map string section内)。
注意,以上的函数有个特点,就是可以任意提供string key,假设这个key已经存在于字典中,就返回字典内所对应的的value,如果提供的key不在字典中,就会新建一个(key, value) pair,并返回这个新建的value。比如$T("Force 1")会得到4,因为已经存在了。而$T("y98w3gf9")会得到3(同时在字典中新建一项"y98w3gf9": 3)。
3. Location dictionary
"Location 1": 1
"Location 2": 2
"Location 3": 3
...
"Anywhere": 64
...
与之相关的函数:
$L("key")或EncodeLocation("key")或GetLocationIndex("key")
注意,触发中的Location对应的数字是它的旧版location index + 1,即CreateUnit(1, 0, "Location Name", 0)等同于CreateUnit(1, 0, $L("Location Name") + 1, 0)。
但是,euddraft0.9.0.0已经更新了该字典,"Location 1": 1, "Location 2": 2,即CreateUnit(1, 0, "Location Name", 0)等同于CreateUnit(1, 0, $L("Location Name") + 1, 0)
故相同的代码使用不同版本的euddraft编译的结果会不同。
Location name所对应的string的stringID被储存在0x0058DC60,即location table中。具体的string本来应该储存在MapString中,但是euddraft将location name所对应的string都删除了,并且把location table中所有string的stringID都改成了0。
所以也没有能够返回location name对应的string的地址的函数。
4. Unit dictionary
包含tbl字典的前228项,只不过index从0开始,我们习惯把这个叫UnitID。还包含所有自定义的单位名
"Terran Marine": 0
"Terran Ghost": 1
"Terran Vulture": 2
"Terran Goliath": 3
"Goliath Turret": 4
"Terran Siege Tank (Tank Mode)": 5
...
"Terran Vespene Gas Tank Type 2": 227
"Any unit":229
"Men": 230
"Buildings": 231
"Factories": 232
"Custom name for Terran Marine": 0
"Custom name for SCV": 7
...
这个字典的特点就是存在“多对一”的情况。
相关函数:
$U("Unit name")或EncodeUnit("Unit name")或GetUnitIndex("Unit name"):
比如把在scmd中把枪兵的名字改成了"AAA",那么$U("Terran Marine")和$U("Terran Marine")都会得到0
没有返回字符串地址的函数。
5. Switch dictionary
"Switch 1": 0
"Switch 2": 1
...
"Switch 256": 255
"Custom name for switch 35": 34
...
该字典与unit字典类似,不做赘述。
相关函数:
$S("Switch name")或EncodeSwitch("Switch name")或GetSwitchIndex("Switch name")
没有返回字符串地址的函数。因为euddraft把自定义switch的名字所对应的string从MapString中删除了,只有字典有用。
综上,5个字典中只有前两个字典包含“返回字符串地址”的函数。5个字典都是在scmd编译地图的过程产生,并且字典里的元素不能够被删除。map string字典还可以通过自定义的string来扩充。
注意,stat_txt.tbl除了本身是一个字典之外,还对应了一个tbl string section(在内存中),储存了具体的string。
同理,内存中也有map string section,储存了全部map string。
所以我们有5大字典,还有2大string sections(字符串集)。在游戏中能看到的全部文字,都在这两个字符串集里面。
而location、switch只有字典而已,内存中并没有专门储存它们的string,当然也没有获取它们string地址的函数。
总结:
用key来得到value(index)的函数:
$B("TBLkey"), EncodeTBL("TBLkey")
$T("MapStrKey"), EncodeString("MapStrKey"), GetStringIndex("MapStrKey")
$L("Location name"), EncodeLocation("Location name"), GetLocationIndex("Location name")
$U("Unit name"), EncodeUnit("Unit name"), GetUnitIndex("Unit name")
$S("Switch name"), EncodeSwitch("Switch name"), GetSwitchIndex("Switch name")
用来获取string地址的函数:
GetTBLAddr(int), GetTBLAddr("TBLkey")
GetMapStringAddr(int), tct.strptr(int), GetMapStringAddr("MapStrKey"), tct.strptr("MapStrKey")
接下来是介绍读写string的函数:
string的读取:
ptr2s(ptr), tct.str(ptr): return地址ptr内储存的string,跟C++内的char*一样,读到0x00终止符(EOS符,记作\0)才停止读取。
epd2s(PID)是当地址为epd时的读取。armoha推荐使用ptr2s和epd2s
例:
ptr2s(GetTBLAddr("Terran Siege Tank (Tank Mode)"))将会得到"Terran Siege Tank"
string的写入:
dbstr_print(dst, args*):
其中dst(Destination address)代表一个地址,可以精确到byte。并将args*写入该内存地址,并自动在结尾加上EOS符。这个修改是覆盖型、涂抹式的修改。
例:
dbstr_print(GetTBLAddr("Terran Siege Tank (Tank Mode)") + 1, "haha", "ha")
将会把tbl string section里面的"Terran Siege Tank\0"这几个字节改成"Thahaha\0iege Tank\0",即用"hahaha\0"这7个字节来覆盖掉"erran S"这7个字节,导致游戏中坦克的名字变为"Thahaha"。因为游戏中的单位名字读取就是直接读GetTBLAddr("Terran Siege Tank (Tank Mode)")这个地址内的string的。
settbl(tblID, offset, *args): 直接修改tbl string section内对应的string内容,以EOS结尾
settbl2(tblID, offset, *args): 与settbl的区别仅为 结尾不加EOS符
其中settbl等价于dbstr_print(GetTBLAddr(tblID) + offset, *args)
例:
settbl(5, 1, "hahaha")效果等价于上面db_str的例子
而settbl2(5, 1, "hahaha")会把"Terran Siege Tank\0"这几个字节改成"ThahahaSiege\0"
注意,这种改string的方式比较危险,因为如果给的string过长,则可能覆盖掉原有的EOS符,并影响下一个string的内容。
注:
tct.setTbl(int, offset, 0, "str"): (在TE中为EUD Action: ChangeStat),此函数基本等同于settbl2,不要用。
用settbl2就行。
此外还有settblf(tblID, offset, format_string, *args)和settblf2(tblID, offset, format_string, *args)
用法参考format的语法。
其他例子:
wwrite(0x660260 + 2 * 7, $T('\x03New SCV'));可以把scv的名字改为黄色的"New SCV"。
注:以内存0x0066026开始的block储存了全部228个单位的NameStringNumber,如果某单位(例如7号单位SCV)所对应的number为0,则表示该单位使用默认名称,因此要去TBL中寻找名称(scv在TBL中的编号为8);如果该单位所对应的NameStringNumber不为0,则它使用的是自定义名称,要在MapStringSection中读取。此例子中,$T('\x03New SCV')的作用是在MapStringSection中新建一个string "\x03New SCV",给其assign一个编号并返回该编号(同时字典也会增加一项),而内存地址0x660260+2*7中储存的就是scv的NameStringNumber。
注意,触发中使用的location序号要在上面的基础上加1。比如locationX的序号为0,那么在触发中如果想使用locationX,就要写1,比如CreateUnit(2, 0, "locationX", 7)就是create 2 Terran Marine for player 8 at "locationX"。或者可以写CreateUnit(2, 0, $L("locationX"), 7)
另外,在scmd中如果删除了某个location,并不会使得该location之后的location序号依次前移,而是保持原来的序号不变。比如location列表中有locationA, locationB, locationC,序号分别为0, 1, 2,此时删除locationB,那么还剩两个location,此时locationC的编号仍然是2。所以,不能根据左侧location的位置来判断location序号,一定要用上面的函数来决定location序号。
Db(length); //凭空创建一个string,预留length字节的空间,返回该string的起始地址
例:
const s = Db(256); //在内存中分配256字节的空间准备储存string,将地址赋值给s。类似于C++的char* s;
dbstr_print(s, "haha"); //写入内容"haha"
当然,这里有比dbstr_print更高端的写入string的方式:
sprintf(dst, "format_string", *args); //其中dst代表string的地址
例:
const chat = Db(256);
const cp = getcurpl();
sprintf(answer, "{:c}{:n}:\x07 Hello!", cp, cp); //之后在StringBuffer里面会细讲
strlen(dst);//输入地址,输出max(string的长度 - 4, 0),比如一个string有4个字符就输出0,有7个字符就输出3
//不知道为何要减4,可能是bug,需要向开发人员反映
关于StringBuffer object:
const s = StringBuffer(255); //建立一个StringBuffer object,名字是s,初始化255(byte?)的空间
注意一个StringBuffer object在初始化之后,每个player都会独有一个,即StringBuffer内的所有methods都仅对当前玩家生效。
setcurpl(0);
s.insert(0, ""); 或者s.insert(0);//在player0的s的起始位置插入"",否则不能正常使用append
s.append("ab",2, "c"); //把player0的s变为"ab2c",等价于s.appendf("ab{}c", 2);
s.append("de"); //把player0的s变为"ab2cde"
s.insert(1, "x"); //从位置4开始,将字符替换为x,即s变成了"ab2cx",注意,insert的第一个argument是EPD,所以只能4个4个来
setcurpl(1);
s.insert(0);//在player1的s的起始位置插入"",否则不能正常使用append
s.append("xy"); //把player1的s变为"xy"
s.Display(); //在player1的屏幕内打出"xy"
setcurpl(0);
s.Display(); //在player0的屏幕内打出"ab2cx"
s.DisplayAt(0);//在player0的聊天栏最上面那一行打出"ab2cx"
s.DisplayAt(10);//在player0的聊天栏最下面那一行打出"ab2cx"
s.StringIndex; //一个member,记录该StringBuffer内的string的编号,与GetMapStringAddr连用可得到地址:
即GetMapStringAddr(s.StringIndex)
gettextptr(): 返回0x00640B58所储存的数值,即Next Display Text Line
dst: 函数argument中出现的dst(即Destination address)是指string address
例:parse(dst, radix = 10) 就是说,第一个argument要输入一个address
simpleprint(*args); //即simpleprint(variable1, "text1", variable2, "text2", ...); //给每个玩家都print对应的内容,args之间以空格分隔
等价于
const cp = getcurpl(); //Remember current cp value and later set back
setcurpl(getuserplayerid());
tct.print(variable1, " ", "text1", " ", variable2, " ", "text2", ...);
setcurpl(cp); //Set back to previous value
也大概等价于
EUDPlayerLoop()();
tct.print(variable, " ", "text");
EUDEndPlayerLoop();
DisplayTextAt(line, "content");
//DisplayTextAt(0, "Hello"), Display "Hello" at the upper most line for current player
//line可以是0 - 10的任意值,因为普通message有11行
eprintln(*args); //在error line去显示message,即最下方正中央的位置
eprintf(format_string, *args);
//eprintf("a{:s}b", GetTBLAddr(2)) 等价于 eprint("a", ptr2s(GetTBLAddr(2)), "b");
关于format string:
每一个跟string有关的函数都会有一个format版本,比如任何print都会有printf
"format"的使用规则跟C语言的printf类似,我们以eprintln与eprintf为例:
eprintln(*args): 在error line去print一串东西,*args代表该函数可以接受任意个数的argument
例:
const a = 2;
const b = 3;
eprintln("hello ", a, "Yes", "OK", b, ptr2s(GetTBLAddr(2)));
这个的效果就是在error line打印出"hello 2YesOK3Terran Ghost"
eprintf(format_string, *args):
format_string就是要print的东西,里面会掺杂着一些“format”,这些format要被后面的*args所代替。因此,args的个数要等于format_string中format的个数。format用{}来表示,大括号内写不同的flag会表示不同的format
例:
eprintf("hello {}YesOK{}{:s}", a, b, GetTBLAddr(2));
这个就是会跟eprintln("hello ", a, "Yes", "OK", b, ptr2s(GetTBLAddr(2)))输出完全相同的东西
可以看到,eprintf中的format_string里面有3对大括号,代表是3个format,因此后面必须紧跟3个arguments,分别来替代这3个format。{}就是让后面对应位置的argument必须是一个普通变量(数字,字符串,或者整数型变量名),而{:s}就是要求后面对应位置的argument必须是一个Address(且最好是储存着string的address)。
在此例中,一共有3个format,因此后面紧跟3个argument,第1个arg代替第1个format,第2个arg代替第2个format,第3个arg代替第3个format,即a的值用来代替第1个{},b的值用来代替第2个{},而GetTBLAddr(2)地址所储存的string代替了{:s},当所有的format都被代替之后,才能得到我们最终要显示的string
再以SetPName / SetPNamef 为例:
const title = EPD(GetMapStringAddr($T("军官")));
const level = 5;
const cp = 0;
SetPName(cp, epd2s(title), " \x07level:\x04 ", level, " ", PColor(cp), PName(cp)); //方法1
SetPNamef(cp, "{:t} \x07level: \x04{} {:c}{:n}", title, level, cp, cp); //方法2
注:
SetPName/SetPNamef函数用来修改玩家的名字,第一个arg的cp就是指出要修改谁的名字,比如SetPName(0,...)就是要修改玩家1的名字。效果就是他说话的时候,他的名字会变成我们给它设定的名字。本质上来讲,SetPName改的是玩家的聊天栏中的玩家名,而不是0x0057EEE0储存的PName(cp)。所以必须保证每轮触发都执行SetPName才能保证玩家在聊天栏看到的玩家名是被更改后的玩家名,并且建议使用eudTurbo。同理,即使用了SetPName,那么PName(cp)仍然是玩家本来的名字,不会变。注意,此函数是shared,即每个玩家的聊天栏都会被改动。
在此例中,如果玩家1名字是ABC,说出了"Hello",那么聊天栏会显示:
ABC: Hello
然后当SetPName被执行时,聊天栏显示的内容变为:
军官 level: 5 ABC: Hello
其中"军官"和"ABC"的颜色都是玩家1本身的颜色,"level 5"是\x07所对应的颜色,"5"是\x04所对应的颜色。
解释:
title, level, cp, cp分别对应{:t}, {}, {:c}, {:n}
其中{:t}接受一个EPD,输出该EPD内存储的string,等同于epd2s(input)
{}是接受什么就输出什么
{:c}是接受0-7之间的某个数字,输出该玩家所对应的颜色,等同于PColor(input)
{:n}是接受0-7之间的某个数字,输出该玩家的游戏名,等同于PName(input)
综述:
Supported Format Types:
c : Player color of the corresponding number (= PColor)
n : Player name of the corresponding number (= PName)
s : String address to connect to (= ptr2s)
t : EPD address of string to connect (= epd2s)
x或X: output value in hexadecimal notation (= hptr)
其他例子:
dbstr_print(dst, *args)所对应的是sprintf(dst, format_string, *args)
stat_txt.tbl的结构(英文版):
开头的2字节(u16): table中含有的entries总数。该值为1547
之后的2字节(u16): 第1个entry的string在stat_txt.tbl中的位置,该值为2+2*1547=3096
之后的2字节(u16): 第2个entry的string在stat_txt.tbl中的位置,该值为2+2*1547+第1个entry的长度
...
stat_txt.tbl + 1547*2字节: 第1547个entry的string在stat_txt.tbl中的位置
stat_txt.tbl + 2 + 1547*2字节: 第1个entry,即"Terran Marine<00><2A><00>Ground Units<00>"
Memory(Address, cmptype, value); //Memory(0x0058D6F8, AtLeast, 10), elaspsed time is at least 10 game seconds
MemoryX(Address, cmptype, value, mask); //MemoryX(0x0057F1E0, Exactly, 256, 0x0000FF00), there is exactly one active human player in the game
注意,由于该函数只能4字节4字节地读,所以在此例中address填写0x57F1E1,0x57F1E2,0x57F1E3都相当于写0x57F1E0。
RemoveUnitAt(number, UnitID, LocationIndex, PlayerID);
注意,EE的GUI里面是先Player再Location,number=0就是All
KillUnitAt, CreateUnit同理,都是先location再playerID
[Unlimiter]的side effect:
(1)就是让图像重合的部分反复闪烁,这一点在plugin manual里面有说明
(2)每个CUnit的sprite指针全部变为0,即无法通过CUnit来改变单位颜色了
检测是否单人游戏:
dwread(0x0057F0B4)=dwread_epd(-11436)==0: single player
dwread(0x0057F0B4)=dwread_epd(-11436)==1: multiplayer
Preserve trigger(trigger末尾把timer设为0)貌似会使得simpleprint不正常。这种情况要用setcurpl(getuserplayerid())来代替
4个OB位的playerID:
128 129
130 131
把单位的placement box (EUDDB里面说的是building dimensions)改成0,0 可以让建筑变成invisible,改成31,31或以下,则可以让建筑放在任何地方,也可以叠任意数量。可以用这种方法来create叠起来的单位
Create兵时,兵的排布取决于unit size而非placement box,只要unit size足够小,create出来就可以聚在一起,不会unplaceable
关于使用order造兵:
对于虫族来说,把一个larva的order(+0x04D)设为42、orderState(+0x04E)设为0、buildQueue[0](+0x098)设为目标单位即可使其变成Egg并孵化成目标单位。同理,对于刺蛇来讲,把它的order设为42、orderState设为0、buildQueue[0]设为103即可让它孵化成lurker,把飞龙的order设为42、orderState设为0buildQueue[0]设为44则可以让他化茧成guardian
注意!给了order之后,larva会在下一个frame才会变成egg,所以如果在同一轮扫触发中用若干次loopUnit来造兵的话,必须对于每个larva都进行buildQueue0的判断,只有buildQueue0==228的才能造兵
当Larva(ID=35)的order为77时(larvaIdle),它的orderState=0代表它在基地左侧,orderState为1、2、3分别代表它在基地的上、右、下侧。按S能左移Larva的原因就是因为S指令会使得单位的orderState变成0,因此也就把larva移到了基地左侧。
地图加密技巧:(防止用smc破解)
在地图文件名中加入这个字符:加密ˍ2
eud的CenterView() function在单人模式下自带preserve,如果在eud中使用了CenterView,那么玩家用单人模式打开之后,CenterView生效之后玩家视角会被锁定指定的视野,鼠标可以动,但是屏幕不能移动。直到运行了scmd内的CenterView才能解除视角锁定。
种族为User select时,如果选择虫族,则会在开局给玩家1个基地4个农民和1个房子。房子的位置跟start location的位置有关。
如果start location位于地图第一象限,则房子在基地左下;
如果start location位于地图第二象限,则房子在基地右下;
如果start location位于地图第三象限,则房子在基地右上;
如果start location位于地图第四象限,则房子在基地左上。
对于“事先分号队伍、在游戏中随机点位”的宏图,必须用p12的3族基地重叠在矿区,然后give,因为那里没有办法create或者move基地。这样的后果就是,基地的位置可以造建筑。为了对抗这个后果,可以用人工kill法:
画一个location,这个location要刚好圈住菊花,12格不能多也不能,然后勾上“地面”的flag。由于区域的右边界和下边界是会殃及到相邻格子的,所以我们要把location的右和下边界都缩回1像素,才能保证旁边的building不被kill。触发:
conditions = {
游戏开始
all player command at least 1 hatchery at 菊花位1
}
actions = {Kill所有player的非基地建筑}
conditions = {
游戏开始
all player command at least 1 command center 菊花位1
}
actions = {Kill所有player的非基地建筑}
conditions = {
游戏开始
all player command at least 1 nexus 菊花位1
}
actions = {Kill所有player的非基地建筑}
总共24个触发。
之所以要用all player,是因为我们无法事先得知每个点位分别是p几
Command条件在单人模式下有延迟,在多人模式下正常。
getuserplayerid(): 内存0x512684,注意不是0x57F1B0
CurrentPlayer内存:0x6509B0
关于录像的自动保存:
星际自动保存的游戏录像的文件名的长度是被限制在35个字符以内的,文件名样式为
hhmmss,地图名.rep
其中,“hhmmss”是6个数字,记录的是录像在本日的时间,比如13时45分56秒,就是134556,后面的地图名就是照抄地图文件名。可以发现,地图名的部分最多只能有28个byte,所以,如果地图文件名超过28byte,则会省略之后的内容。如果地图文件名含有utf8字符(比如中文、韩文等),长度为3byte,并且在切28byte时刚好把这个大字符砍断,那么该录像就无法正常显示在星际中。比如,地图文件名为“12345678901234567890123456啊”,前面的数字为26byte,而“啊”的长度为3byte,因此录像文件名的字符长度限制就会导致“啊”字只能保存前两个byte,导致“无法看到录像”的问题发生。
此外,星际游戏录像的在命名时,如果在地图文件名中碰到了“.”,则舍弃“.”之后的内容。比如地图名为“123456啊.啊啊啊啊啊啊少时诵诗书所所所所所所所所.scx”的录像文件名也是为“xxxxxx,123456啊.rep”
0x006D0F38(Replay Header - Rand Seed)里面储存的值是“主机玩家的系统时间距离1970年1月1日的秒数”而不是“本电脑的系统时间......”。在游戏中所有玩家的此内存值相同。所以破解限时地图很简单:只需要把自己的系统时间改早,然后自己建主机就行了,主机里面所有人都能玩
神族单位被攻击时:当shield整数部分小于1时,会被视为无盾状态,此时受攻击将不会触发盾防,且直接扣血。当shield整数部分大于等于1时,受攻击则会触发盾防,并且先扣盾再扣血。
hp的显示是向上取整,而shield的显示是向下取整。
各种常数:
Energy魔法回复:+8每帧
虫族血量回复速度:+4每帧(即4/256个血)
神族能量电池能量回复:被加盾的单位+1280每帧
神族能量电池能量消耗:能量电池自己-640每帧
神族shield回复速度:+7每帧(即7/256个盾)
护士加血:+200每帧
护士加血耗能:-100每帧
鬼兵隐形耗能:-18每帧
隐飞隐形耗能:-21每帧
建筑燃烧减血:-20每帧
参考资料:
https://liquipedia.net/starcraft/Game_Speed
EUD图删掉的sections:
TILE: StarEdit Terrain, width * height bytes
ISOM: Isometric Terrain, (width / 2 + 1) * (height + 1) * 4 * 2 bytes
DD2 : StarEdit Sprites (Doodads)
SWNM
CHK sections comparison:
eudraft
VER :
TYPE: n
IVE2: n
VCOD:
IOWN: n
OWNR:
SIDE:
COLR:
ERA :
DIM :
MTXM:
TILE: n
ISOM: n / destructed 28 66 17 CE 53 54 55 42 00 10 00 00 00 00 30 36 CB 71 4E 96 B4 70 8A E9 6F 62 17 78 40 E1?
UNIT:
PUNI:
UNIx:
PUPx:
UPGx:
DD2 : n
THG2:
MASK:
MRGN: 把所有location的string index全部变成0
STRx:
SPRP:
FORC:
WAV : n (?)
PTEx:
TECx:
MBRF:
TRIG:
UPRP:
UPUS: n
SWNM: n
住:如果一个地图删掉了TILE、ISOM、DD2,则还原的时候可以新建TILE然后照抄MTXM,新建ISOM然后把所有字节设为0,新建DD2,长度设为0,即44 44 32 20 00 00 00 00
新建空的STR需要12字节:53 54 52 20 04 00 00 00 00 00 00 00
作图触发tips:
开局根据种族送兵时,condition要加上timer限制。
关于low medium high ground/air:
不管是哪个tile set,都会有低地地形、中等地地形、高地地形。
地面单位站在高地上,就是high ground,站在中地上,就是medium ground,站在低地上,就是low ground
空中单位飞在高地正上方,就是high air,其他同理
注:这个“地面单位”和“空中单位”指的是单位的advanced flag中的flying unit属性,而不是指elevation level
注意:
RemoveUnit动作触发时,单位瞬间消失,但人口需要延迟1帧才会下降。
CreateUnit动作触发时,单位瞬间被创建,且人口会瞬间上升(无延迟)
所以,当使用scmd的hyper trigger(扫触发间隔为2帧)时:
Trigger {
players = {P1},
conditions = {
Always();
},
actions = {
RemoveUnitAt(1, "Terran Medic", "Anywhere", CurrentPlayer);
CreateUnit(1, "Terran Medic", "Anywhere", CurrentPlayer);
PreserveTrigger();
},
}
的效果是:人口为1, 2, 1, 2, 1, 2......交替变化
Trigger {
players = {P1},
conditions = {
Always();
},
actions = {
CreateUnit(1, "Terran Medic", "Anywhere", CurrentPlayer);
RemoveUnitAt(1, "Terran Medic", "Anywhere", CurrentPlayer);
PreserveTrigger();
},
}
的效果是:人口为0, 1, 0, 1, 0, 1......交替变化
关于人族附件:
编辑器里面把它预设成P12的,那么随便来一个人族基地落在它旁边都能占有它。
如果编辑器里面把它预设成P1(或P2)的,那么它在游戏里则永远属于P1 (或P2),即任何基地落在旁边都不能跟他们相连
永远都无法用GiveUnit触发来将这个雷达附件转移所有权
All unit count table:玩家拥有的所有单位的数量(包含正在建造中的)
Completed unit count table:玩家所拥有的所有已经建造完成的单位的数量(不包含正在建造中的)
bring 和 command 条件(在绝大多数情况下)都只检测已经建造完成的单位,即查看completed unit count table
注:经试验,只有Command(.., AtMost, .., ..); 会查看All unit count table,其他的情况都是查看completed unit count table 。此为Command AtMost的"bug",应该是为了在对战时如果某个玩家只剩最后一个未完成的建筑时不判负
而kill、remove、give、move location等都会检测所有单位(包含未建造完成的单位),即查看All unit count table
刺蛇所变的zerg lurker egg属于未建造完成的zerg lurker egg,而这个蛋变成lurker之后,就变成了已经建造完成的lurker
所以游戏中,玩家永远不可能拥有“已经建造完成的zerg lurker egg”,所以,bring 和 command condition 永远也检测不到玩家的zerg lurker egg
scmd编辑器中可以直接在地图中摆放zerg lurker egg,而这个蛋在游戏中会永远保持蛋的状态,永远不会变成孵化成lurker,这种蛋才属于真正的“已经建造完成的zerg lurker egg”,并会体现在completed unit count table中,并会被bring和command检测到。
另外:
Z农民造基地时,Zerg Hatchery是All unit count=1, cmp=0,造好了之后两个都是1
Hatchery升级Lair的过程中,仍然视为是Hatchery,升级完成的一瞬间,视为Lair complete。因此不存在"lair all=1, cmp=0"的情况。
同理,Lair升级成Hive的过程中,仍视为Lair。因此不存在"Hive all=1, cmp=0"的情况。
Bring和Command的区别:
Bring无法检测到正在升级的虫族建筑(升级中的龙塔、基地)
Command可以检测到。
当Hatchery正在升级Lair时,Bring检测不到Hatchery,而command可以检测到Hatchery
Hatchery升级完成变成Lair时,Bring和Command同时检测到Lair
Lair正在升级成Hive时,Bring检测不到Lair,而command可以检测到Lair
Lair升级完成变成Hive时,Bring和Command同时检测到Hive
且
注意,Bring AtMost有重大bug
(1)
"Air"为一个location,只勾上了low/med/high Air这3个flag
当P1有小狗进运输机时
Bring(P1, Exactly, 0, "Zerg Zergling", "Air"); 判定为False
Bring(P1, AtMost, 0, "Zerg Zergling", "Air"); 判定为True
所以,这个时候需要用Exactly
(2)
当玩家只有1个Hatchery,且该Hatchery正在升级为Lair时:
Bring(P1, AtMost, 0, "Zerg Hatchery", "Anywhere"); 判定为False
Bring(P1, Exactly, 0, "Zerg Hatchery", "Anywhere"); 判定为True
而当玩家没有任何基地时,二者都判定为True
当玩家只有2个Hatchery,且其中一个Hatchery正在升级为Lair时:
Command(P1, AtMost, 1, "Zerg Hatchery"); 判定为False
Bring(P1, AtMost, 1, "Zerg Hatchery", "Anywhere"); 判定为False
Bring(P1, Exactly, 1, "Zerg Hatchery", "Anywhere"); 判定为True
所以,在这种情况下,Bring AtMost等同于Command AtMost
此外,关于Bring Condition Bug:
下列只有CFGH可以成功计数,剩下4个都不行。所以推荐使用Bring+Remove无关Unit 的组合:
-----
--(A)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Command(P1, AtLeast, x, "Terran Marine");
},
actions = {
RemoveUnitAt(x, "Terran Marine", "Anywhere", P1);
SetResources(P1, Add, x, Ore);
DisplayText("(A) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(B)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Command(P1, AtLeast, x, "Terran Marine");
},
actions = {
GiveUnits(x, "Terran Marine", P1, "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(B) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(C)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Bring(P1, AtLeast, x, "Terran Marine", "Anywhere");
},
actions = {
RemoveUnitAt(x, "Terran Marine", "Anywhere", P1);
SetResources(P1, Add, x, Ore);
DisplayText("(C) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(D)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Bring(P1, AtLeast, x, "Terran Marine", "Anywhere");
},
actions = {
GiveUnits(x, "Terran Marine", P1, "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(D) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(E)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Command(P1, AtLeast, x, "Terran Marine");
},
actions = {
RemoveUnitAt(x, "Terran Marine", "Anywhere", P1);
RemoveUnitAt(1, "Terran Medic", "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(E) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(F)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Command(P1, AtLeast, x, "Terran Marine");
},
actions = {
GiveUnits(x, "Terran Marine", P1, "Anywhere", P12);
RemoveUnitAt(1, "Terran Medic", "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(F) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(G)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Bring(P1, AtLeast, x, "Terran Marine", "Anywhere");
},
actions = {
RemoveUnitAt(x, "Terran Marine", "Anywhere", P1);
RemoveUnitAt(1, "Terran Medic", "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(G) Add " .. math.floor(x) .. " Ore");
},
}
end
-----
--(H)
-----
for i = 9, 0, -1 do
x = 2^i
Trigger {
players = {P1},
conditions = {
Bring(P1, AtLeast, x, "Terran Marine", "Anywhere");
},
actions = {
GiveUnits(x, "Terran Marine", P1, "Anywhere", P12);
RemoveUnitAt(1, "Terran Medic", "Anywhere", P12);
SetResources(P1, Add, x, Ore);
DisplayText("(H) Add " .. math.floor(x) .. " Ore");
},
}
end
Armoha, 2022-01-10:
- Both do not count units in training
Bring + AtMost
- Do not count units in transit
- Count building on construction
- Count Cocoon, Egg, Lurker Egg
- Ignore location elevation settings
Bring + Exactly/AtLeast
- Count units in transport
- Do not count building in construction
- Do not Count Cocoon, Egg, Lurker Egg
- Both count units in transit
Command + AtMost
- Count building on construction
- Count Cocoon, Egg, Lurker Egg
- Count units in training
Command + Exactly/AtLeast
- Do not count building on construction
- Do not count Cocoon, Egg, Lurker Egg
- Do not count unit in training
^ eps negate conditions on compile time except those
关于扫出来的雷达特效:
当P1扫雷达时,cmp会加1个scanner sweep (当然all也有),过一段时间后,该unit消失
但是command可以检测到scanner sweep,而bring(to anywhere)检测不到。
Scanner Sweep Effect (雷达特效):
星际eud图的绝大多数特效都是由Scanner Sweep得来的
基本步骤:
onPluginStart:
SetMemoryX(0x661558, SetTo, 0x20000, 0x20000); //Scanner sweep: can create
想Create特效时:
SetMemoryX(0x666458, SetTo, xxx, 0xFFFF)// SpriteID 380 (Scanner Sweep Hit): imageID SetTo xxx
调整imageID为xxx的各种参数,比如IscriptID(一般设为250)和draw func(控制颜色)
CreateUnit(1, "Scanner Sweep", "loc", player)
还原imageID为xxx的各种参数
SetMemoryXEPD(EPD(0x666458), SetTo, 546, 0xFFFF), // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546
RemoveUnitAt(All, 33, 'loc', player)
RemoveUnitAt和KillUnitAt的重大bug:
"loc"为任意一个location,对任意正整数x,有:
KillUnitAt(x, "Terran SCV", "loc", P1);
如果这条Action生效并杀到了任意一个在"loc"内已装机的SCV,则"loc"内所有的P1的已装机的SCV都会被杀掉/清除。
其他可装机的单位同理,其他玩家同理,RemoveUnitAt同理。
造兵时,自己取消造的兵,退全款;出兵建筑被打掉,也退全款。
升级科技时,自己取消升级,退全款;升级建筑被打掉,退75%钱。
关于Z基地菌毯蔓延问题:
编辑器内使用地形或者建筑围住基地,可以使菌毯不蔓延。但是,如果在基地的固有菌毯范围内放置虫族建筑,则会导致菌毯无视地形、无视建筑蔓延开来。
用kill或者remove触发杀掉的单位通通不会算入death数,如果房子里面装着兵,然后用kill/remove触发杀掉房子,则房子里面的兵也不会算入death数。
奇怪的现象:
只要触发列表里面有一个SetResource触发,那么单人模式下,只要按ESC就可以强迫加速触发
注意:AllPlayers仅包括在游戏中的所有玩家(含neutral players)
若P2不存在于游戏中,但是地图本身有P2的start location,则GiveUnits(..., P2)会将单位give给P2,此时该单位自动敌对于人类玩家,并且属于P2的单位,不属于AllPlayers的单位。GiveUnits(AllPlayers, ..., Px)无法将该P2单位Give给Px
KillUnitAt(All, 14, "Anywhere", P1);与KillUnit(14, P1);的区别:
前者无法清除P1在核弹井里面造好但还未发射的核弹,而后者可以。因为在核弹井里面造好的核弹不在CUnit链表中,自然也就不存在于地图上的任何地点,它仅仅代表P1拥有1枚核弹,体现在AllUnitCountTable和CompletedUnitCountTable中,并且Command会生效。而发射出来的核弹才真正会进入CUnit链表,并且拥有具体的位置,orderID为125,orderState从1变到2再变到4
Disable freeze:
[freeze]
freeze: 0
单位名称使用utf-8读取:
[main]
decodeUnitName : utf-8
关于字符串的编码:
Starcraft uses "utf-8" as the highest priority when decoding strings in string section,
but uses "cp949" as the highest priority when decoding strings in stat_txt.tbl
即,对于每个string,SC:R都会以以下优先级进行decode:
map string: utf8 > cp949 > windows-1252
stat_txt: cp949 > utf8 > windows-1252
所以,要想在TBL里面使用简体中文,需要加上"\u2009"字符,即"0xE2 0x80 0x89",即:
settbl("Terran Marine", 0, py_str("简体中文\u2009").encode("UTF-8")); //或者
dbstr_print(GetTBLAddr("Terran Marine"), "简体中文\u2009\x00");
euddraft更新至0.9.1.4版本之后:
settbl("Terran Marine", 0, "简体中文", encoding=py_str("utf-8"));
经过修复之后:
settbl("Terran Marine", 0, "简体中文", encoding="utf-8");即可
关于按钮问题:
按钮的操控内存在0x5187E8, EPD=-116447,是个250x12的数组,总共250个ButtonSet,每个ButtonSet有12字节,但是这12字节只有前8字节有用,后4字节是unknown。每个ButtonSet的前4字节是该ButtonSet的Number of available buttons,数量可以超过9个;中央的4字节是buttonInfo指针,即该ButtonSet的信息所在的地址。默认情况下,这些地址都是固定不变的,并且不同ButtonSet的buttonInfo指针可能指向同一个地址(比如0号和20号buttonset)。因此,如果需要更改某个ButtonSet的按钮信息,有两种更改方式:
(1) 首先更改该ButtonSet的按钮数,直接修改button Info的内容。由于不同button set的buttonInfo pointer可能指向相同地址,所以这样可能导致同时修改了多个button set
(2) 首先更改该ButtonSet的按钮数,然后将自定义的按钮信息注入随便一个地址内,最后将该地址写入ButtonSet的+0x04 offset。这种方法比(1)更好,因为可以确保不影响其他buttonSet
在实际操作过程中,有时需要共同使用以上两种方法。比如我们的需求是不断修改0号ButtonSet(枪兵的button set)的第5个按钮的图标,则需要在第一轮扫触发时应用方法(2),开辟一个新的内存来储存0号button set的button info,使其不影响20号button set。然后再直接更改新的内存内的button info内容。注意,更改时要不断access to 该button set的指针,否则button info无法及时刷新,详见下面的代码。
关于按钮信息(Button Info):
每个按钮需要20字节,所以如果该ButtonSet有3个按钮,则该ButtonSet的按钮信息总共要60字节。每个按钮的按钮信息的第一个字节表示这个按钮的位置(1-9),第3、4字节为icon的ID,之后4个字节为condition,4个字节为action,详见:
[ pos ] [icon ] [ Condition ] [ Action ] [ConVa] [ActVa] [AvTBL] [UnTBL]
001 000 041 000 096 142 066 000 144 055 066 000 041 000 041 000 066 002 000 000
002 000 037 000 096 142 066 000 144 055 066 000 037 000 037 000 063 002 226 002
003 000 042 000 096 142 066 000 144 055 066 000 042 000 042 000 067 002 000 000
以上数据是以Larva的button set的前三个按钮(农民、小狗、房子)为例。
改ButtonSet的方法:用EE手动更改按钮,然后编译,然后在EudEditor.py(若EE3,则是ExtraDataEditor.py)里面找到对应的代码,然后复制粘贴。下面以eps举例:
const bytebuffer = py_eval('bytearray([1,0,41,0,96,142,66,0,144,55,66,0,41,0,41,0,66,2,0,0,3,0,42,0,96,142,66,0,144,55,66,0,42,0,42,0,67,2,0,0,9,0,46,0,96,142,66,0,144,55,66,0,46,0,46,0,71,2,232,2])'); //ButtonSet信息
const btnptr = Db(bytebuffer); //ButtonSet信息
SetMemoryEPD(-116447 + 3 * 35 + 1, SetTo, btnptr); //将ButtonSet信息所在地址写入larva的ButtonSet
SetMemoryEPD(-116447 + 3 * 35 + 0, SetTo, 3); //larva里面只有3个按钮
如果想要直接修改按钮信息,不新建指针,则需要:
// marine_epd为自定义button set0按钮信息的新地址的epd值
SetMemoryXEPD(marine_epd + 5*4, SetTo, 7, 0xFFFF); // 将button set中的第4个按钮(从0开始数)的pos改成7
SetMemory(0x5187EC, Add, 0); // 必须access to buttonSet 0的button info指针,这样才能强制刷新按钮信息
按钮的Condition为Always时,按钮总是亮的,可以按。
按钮的Condition不是Always时,当此Condition不满足时,按钮可能是灰的,也可能消失。
当Condition为4(Can Create Unit/Building)时,比如Condition Value为95 (Ragnasaur),假设ragnasaur的requirement仅为"Must have 某个unit X"时,则当玩家不拥有Unit X时,按钮为灰;但是如果ragnasaur的requirement较为复杂,则当该requirement不满足时,按钮消失。
当Condition为23(Spell Researched)时,比如Condition Value为20(Hallucination),假设techdata - Hallucination - requirement 设为了Must be researched,则当玩家research了Hallucination时,按钮是亮的;当玩家没有research Hallucination时,按钮是灰的。
如果Hallucination的requirement保持默认,则当玩家research了Hallucination时,按钮消失;当玩家没有research Hallucination时,按钮是灰的。
按钮的Action决定了这个按钮按下去之后的效果,当Action为Create Unit时,比如Action Value为95,则当ragnasaur的requirement不满足时,按钮按下去没有实际效果。
[DataDumper]的语法:
temp\stat_txt.tbl : 0x6D5A30, copy
含义是将目标file的内容copy到0x6D5A30内所储存的pointer所point的地址
注:0x6D1238和0x6D5A30两个内存内储存的pointer都是0x19184660,即stat_txt.tbl所在的内存地址。其中dataDumper.py所用的地址是0x6D5A30
关于RequiredData:
即Requirements,总共有5大块,从0x514178开始,占用空间为(连续的内存空间)1096+840+320+688+1316=4260字节。故EE生成的requiredData文件大小为4260字节。此外,由于req data的每一项都是OPcode,长度不一,因此牵一发而动全身,所以改动了req data的同时,也要在0x660A70, 0x6558C0, 0x656198, 0x6562F8, 0x665580这5块地方改动,这5块分别记录5种req data的每一项req所在的offset。这也就是为什么EE2的EUDEditor.py和EE3的ExtraDataEditor.py下面有那么多SetMemory。
所以,当使用EE更改requirement时,除了要把EE生成的requiredData文件复制过去,也要把EUDEditor.py里面SetMemory复制过去。
注:只有EUD地图才有该数据结构!!
RequiredData (data requirement)的字节排列结构,以units req data为例:
整个数据以双00字节开头,以双FF字节结尾。之间的数据分为若干块(block),默认条件下,只有91个unit有req data,所以units req data有91块。
每块数据以该单位的unitID开头(双字节,可有可无),以0xFFFF结尾:
默认条件下的units req data前43字节:
00 00 00 00 02 FF 6F 00 08 FF 05 FF FF FF 01 00 02 FF 6F 00 08 FF 05 FF 75 00 70 00 FF FF 02 00 02 FF 71 00 05 FF 08 FF FF FF
去掉双00字节开头,总共含有3块数据:
00 00 02 FF 6F 00 08 FF 05 FF FF FF
01 00 02 FF 6F 00 08 FF 05 FF 75 00 70 00 FF FF
02 00 02 FF 71 00 05 FF 08 FF FF FF
第1行:00 00为unitID,值为0,表示此数据块为Terran Marine的opcode
第2行:01 00为unitID,值为1,表示此数据块为Terran Ghost的opcode
第3行:02 00为unitID,值为2,表示此数据块为Terran Vulture的opcode
为何说unitID这两字节可有可无?因为0x660A70里面记录的228个offset都是从opcode开始的,而无视掉了unitID这两字节。
简要介绍opcode:
首先,每个opcode的长度都是2字节,连续两个opcode的逻辑关系均为"And",除非有01 FF这个opcode
02 FF: Current unit is ...:
后面紧跟一个unitID,以Terran Marine的opcode为例:02 FF 6F 00就是Current unit is 0x6F,即Current unit is "Terran Barracks"
[unitID 双字节]: (must have ...) 该unitID:
以Terran Ghost为例,75 00前面没有02 FF,所以75 00两个字节是独立出来的,这种独立出来的unitID就是指must have该unitID,所以75 00 70 00连起来的意思就是must have unitID 117 且 must have unit ID 112
03 FF: must have ...
必须紧跟在01 FF之后,见下。
01 FF: Or
一般来讲,后面只能跟着跟02 FF,或者03 FF,或者0F FF。唯一的反例是Tech research Req的32号,Lurker Aspect,里面有一个01 FF后面紧跟了一个unitID双字节,可能是因为之后仍然有opcode,所以语法不一样,尚且无视这个反例。
当后面跟着02 FF时,Or即:current unit is ... 或 current unit is ...,详见航母小飞机(Interceptor)
当后面跟着03 FF时,说明Or之前有一个双字节的unitID,以unit req飞龙为例:
2B 00 02 FF 23 00 8D 00 01 FF 03 FF 89 00 FF FF
其中,8D 00 01 FF 03 FF 89 00 是一个逻辑整体,8D 00就是(must have ...)0D 00,后面的03 FF 89 00就是must have 89 00,合起来就是must have (0D 00 or 89 00),翻译成人话是“must have飞龙塔或者大龙塔”。
08 FF: is not constructing adding-on
05 FF: is not lifted off
13 FF: Can set rally point,
此为39号和40号order所特有
23 FF: Blank
此为upgrade req的unknown60特有。
????: Is Not Training Or Morphing
所有的Upgrades都必须加这个requirement,如果不加的话,会出现如下bug:同一个升级建筑有若干个升级按钮,玩家同时迅速按多个按钮,会导致按钮消失。
其他的opcode略
注:EE2和EE3的req data有区别,EE3生成的req data把所有的表示此数据块的unitID/techID等双字节全部删掉了,所有节省出很多空间。以unit为例,默认情况下总共有91个unit有req data,EE3把他们的unitID双字节删掉之后,总共为Unit req data节省了182字节的空间。
注:从内存中直接提取出来的req data是跟EE2有相同格式的,即有unitID双字节,但是内存中的req data是打乱顺序的,不是按照ID从小到大排列的,具体不知为何。
eps一次性触发的代码:
DoActions(SetResources(P1, Add, 1, Ore), preserved=False);
以下貌似代码全部等价:
const x = [0, 0, 0, 0, 0, 0, 0, 0];
const x = EUDArray(8);
const x = EUDArray(list(0, 0, 0, 0, 0, 0, 0, 0));
以下代码貌似等价:
const x = EUDVArray(8)();
const x = EUDVArray(8)(list(0, 0, 0, 0, 0, 0, 0, 0));
const x = PVariable();
const x = PVariable(list(0, 0, 0, 0, 0, 0, 0, 0));
注意,EUDArray(N)的layout跟death table是一样的,连续的N个4字节数据。
而PVariable()和EUDVArray(8)()是72*8字节,layout跟EUDArray(8)是不同的。
PVariable()是EUDVArray(N)()当N=8时的情况
获取某个EUDVariable的地址:
const x = 0; //创建EUDVariable x
const addr = x.getValueAddr(); //获取x的地址,储存在addr中。即:addr points to x
一般来讲,这些EUDVariable的地址都在0x191xxxxx,比如0x191E24F0, 0x191E2538
print这个字符会使游戏崩溃:ÿ
euddraft0900之后,以下三个等价:
setloc("Location 1", x, y);
setloc($L("Location 1"), x, y);
setloc(1, x, y);
所有Men的Righticlick action (0x00662098 + uID)只能是0,1,2,3,4,5中的一个:
是0的有:[13]Spider Mine, [73]Protoss Interceptor, [91], [92]
是1的有67个
是2的有22个
是3的有:[25]EdDuke Siege, [30]Tank Siege
是4的有:[41]Z农民, [64]P农民
是5的有:[7]T农民
如何使用SCMD查看韩文地图(原图为949编码):
使用-charEncoding=949参数来打开SCMD(这样就能让SCMD使用cp949来解码地图中的字符串),然后拖入韩文地图。然后另存为SC:R格式,即可让SCMD以utf8编码字符串。
接下来必须使用-charEncoding=0参数再次打开SCMD才能恢复默认,否则SCMD将永远默认使用-charEncoding=949
在使用-charEncoding=0参数之后,若想正常使用armoha的TEP,必须使用-charEncoding=65001打开scmd
关于WAV:
"WAV " section含有地图中全部音频的文件名所示stringID。SCMD的Sound Editor就是直接读取的"WAV "。
地图中的音频分为内部音频(virtual sound)和外部音频。内部音频永远都不会体现在scx中,只会体现在STR中。scx的staredit\wav\中存放的音频文件都是外部音频。如果另存为chk,则不会包含任何外部音频。
parse TRIG:
关于starcraft如何读取conditions:
对于每一个trigger,sc读取condition的逻辑为:
for con in conditionlist:
if con.disableFlag:
continue
if con.conditionByte == 0:
break
readcondition(con) # if conditionByte > 23, then condition = Never
即:读取到第一个不disable的condition byte为0的condition之后,无视掉接下来的所有condition
eps代码优化:当dwread内的内存地址是4的倍数且为常数时,使用dwread_epd(EPD(addr))和dwread(addr)是没区别的。当addr为变量时,尽量使用dwread_epd。
例1:
const x = 0x628430;
var curAddr = dwread_epd(EPD(x)); //写法1
var curAddr = dwread(x) //写法2
写法1和写法2完全等价,没有任何区别
例2:
const x = dwread_epd(EPD(0x628430));
var curAddr = dwread_epd(EPD(x)); //写法1
var curAddr = dwread(x); //写法2
写法2比写法1的编译结果多出大约200个objects
eprintln是sync action!!!!
if(getuserplayerid()==0)
eprintln("haha");
会掉线!!
而
if(getuserplayerid()==0)
printAt(0, "haha");
就这正常
eps由boolean转为eudvariable,使用函数l2v:
l2v(MemoryXEPD(0x6CDDC8, Exactly, 2, 2))
string:
字符\u3000
cp949编码:0x1A 0x1A
utf8编码:0xE3 0x80 0x80
[Unlimiter]插件会导致被Create出来的Unit的+0x0C(sprite pointer)变成0,导致无法改unit颜色。所以CreateUnit的代码应该写在unlimiter代码生效之前,只能写在onPluginStart
使用epTrace来debug:
在[main]里面写:
debug: 1
然后编译地图后会生成一个out.scx.epmap,把它拖到epTrace.exe,然后进入游戏玩out.scx
退出游戏后,会生成out.scx.epmap.prof,用txt查看即可
教程:
https://cafe.naver.com/edac/63490
如何让scv以造建筑的形式(点击按钮的时候鼠标上出现绿色/红色的方框里面画着要建造的unit)造任何单位:
搞出相应的按钮,按钮的Condition: 3 (Always), Action: 29 (Build Terran Building), Action value是要造的unitID
然后把要造的unit在units.dat的requirement改成always use或者Current Unit is SCV。(当然可以改mineral/gas数/人口数/BuildingDimension等)这样就可以让scv建造它时收到建造order了(order=30, buildQueue0=unitID)
此时可以检测scv的order和buildQueue然后CreateUnit,之后用Order或者Move触发让SCA停止走动。
如果想让scv直接走过去造,则还需要:
Construction Animation改成0
如何进行“按钮 - 选择目标位置”的触发:
以scv为例子,首先把scv的5个AI-order全部设为2,Right-Click order设为6,然后有如下几种可行的方法:
1. 建造建筑的按钮
(1)给SCV一个按钮,Condition设为Always(或者Can Create Unit/Building,注意改需求),Action设为Create Building - Terran,然后随便设一个Action Value X,然后注意要让单位X的Requirement满足。
(2)检测到scv的order为30(建造)时,获取scv的orderTargetPosition
2. 基本指令按钮
(1)给SCV一个按钮,Condition设为Always,Action设为20 (Patrol)
(2)检测到scv的order为152(Patrol)时,获取scv的orderTargetPosition
注意,按钮Action设为Attack或者move时,由于Attack按钮点到空地和单位上分别会导致order=14(Attack Move)和order=10(Attack Unit)两种,所以检测order时要同时考虑10和14,同理Move点到空地和单位上分别会导致order 6(Ignore)和order 49(Follow)两种。
3. 施法,Tech use
(1)给SCV一个按钮,Condition设为Always,Action设为24 (Use Technology),Action Value设为14(Dark Swarm)
(2)将Dark Swarm所需要的能量设为0 (如果想要让Dark Swarm需要魔法,那么可以把scv变成魔法单位,即Advanced Flag勾上spell caster)
即:SetMemoryX(0x00664080 + 4 * 7, SetTo, 0x00200000, 0x00200000)
(3)将TechData的14号(Dark Swarm)的Usage requirement改成Always use或者Current Unit is Terran SCV(不推荐使用Always Use)。注意:order的119号(Dark Swarm)不用动
(4)检测到scv的order为119(Dark Swarm)时,获取scv的orderTargetPosition,然后将order设回3,并MoveUnit使SCV站定在原位置(否则scv会跑开喷雾)
(5)*注意!在没改脚本(Iscript)的情况下,如果点完按钮后选择的喷雾位置恰好位于scv朝向的张角内且在射程内,则会瞬间出现石头,并且scv消失(因为缺少动画)。所以保险起见,还要将scv所对应的image(247号image)的Iscript ID设为有施法动画的单位的脚本,比如9号脚本(Defiler)或者369号脚本(Corsair)。若设为9号脚本,则SCV会出现蝎子的影子,并且一直在扭动;若设为369号脚本,则scv会出现海盗船的影子,并一直在上下浮动。此时我们可以把930号image(Corsair Shadow)的Drawing Function改成2(Enemy Cloaking),就可以让影子消失,且scv不会上下浮动。
注:完成以上的(1)(2)(3)(5)的操作后,scv就可以真正地喷雾了。喷雾时,scv需要保持order=119的状态持续若干帧才能把雾喷出来,所以如果有(4)的存在(检测到order==119立即改回order=3)则scv无法喷出雾。
如何把Terran Civilian调出Attack按钮,并且能用它来给order并且给完order之后它原地不动:
DatEdit - Unit(Terran Civilian) - AI actions: Attack Unit = 10, Right-click Action = 3
FireGraft - Button Set - Terran Civilian - "Attack" button - Condition = 3 (Always)
注意,使用data dumper来强行改动stat_txt.tbl之后,会使自定义快捷键失效,所以除非万不得已,否则千万不要使用自定义stat_txt.tbl。另外,使用settbl或者settbl2函数也会使得自定快捷键失效。
按钮触发实现方法:
(1)方法一:build check法
EE3 - data edit - Button set - Terran Marine - 激活 button list - 右键 - New code - Train Unit - 随便选择一个value(最好选择英雄Unit,假设选择的unit是Jim Raynor),然后随便选一个图标和注释,Button Work的Condition可以改成3(Always),或者其他。
然后data edit - Unit - Jim Raynor要修改两个:
default: Mineral和Vespene都改成0
Requirements: Always Use,或者Current Unit is "Terran Marine"
然后就可以使用buildcheck和buildreset了
建议使用二模蛤写的BuildCheckConst和BuildResetEPD
(2)方法二:order法
EE3 - data edit - Button set - Terran Marine - 激活 button list - 右键 - New code - Basic Command - confirm
删掉不需要的按钮,然后button work的condition改成Always,action改成Stop reaver
然后orders - Reaver stop - Requirements改成Always,或者Current Unit is "之前设置的拥有这个按钮的unit"
注意,当同时选中两个兵时,右下角的按钮面板会变为244(Units)号Button Set。所以即使你把两个兵单独的button set改成了其他,那么当同时选中两个兵时,button set还是变为244,所以我们一般在改按钮之后还要把244号button set清空:
SetMemory(0x005187E8 + 12 * 244, SetTo, 0);
SetMemory(0x005187E8 + 12 * 244 + 4, SetTo, 0);
注意:
当一个兵的Right-Click order设为6时,选中它之后右键空地的效果是:出现⊙动画,但是兵原地不动;
当Right-Click order设为3时,单位将处于“Can't move”状态,选中之后右键空地不会出现⊙动画,同时也会导致很多order用不了(比如Patrol、Attack用不了)
所以,如果把单位的Right-Click order设为3,则需要将152号Order(Patrol)的requirement改成Always Use才能让单位对于Patrol有反应(单位的CUnit的order ID会变为152号)。注意,由于order requirement本身已经处于1316/1316的爆满状态,如果想更改order,可能首先需要将一些没用的order的requirement设为Don't Use
在使用按钮触发的时候应注意,如果按钮是在建筑上,那么点按钮之后这个建筑或许就真的会造兵(新开辟内存),如果这个按钮在一些men上,那么点按钮之后就不会开辟新内存。
EE2自带的BuildReset function不够严谨
CUnit里面有一个currentBuildUnit (+0xEC)
让任何一个工厂或者你按钮触发的母单位去造一个unit的时候,这个正在被建造的单位都会占用1700个单位中的一个单位,其地址储存在母单位的CUnit的currentBuildUnit中
实际游戏过程中,当我们按ESC取消建造的时候,星际其实做了两件事:
(1) 把母单位(工厂)的buildQueue[buildQueueSlot]设为228
(2) 给currentBuildUnit所指向的单位一个death order,杀掉它(或者直接free这个336字节的内存)
而EE的BuildReset只做了第一件事儿,而没有做第二件事
SetDeaths的单位如果是P9-P12的单位,则在非eud地图中不起任何作用
如果地图是eud图,则会起作用
星际判断一个单位是否在location中,仅跟该单位的Unit Size (Unit Dimension碰撞体积)有关,即内存0x6617C8
CreateUnit触发生成一个兵,取决于它的Unit size而不是placement size(这不废话吗)
玩家用鼠标左键选择单位,点选和框选的判定法则是不一样的。
点选是判定单位的图像Image,框选是判定单位的Unit Dimension (碰撞体积)
如果把龙骑的碰撞体积改成3x3像素(上下左右都设为1像素),那么框选不易选中,而点选就跟之前一样很容易
但是,当单位碰撞体积过大时(大概18格x18格左右),可点选的范围也会缩小,甚至无法点选。具体数值多大才算大,有待研究。
所以,如何让玩家选不中塔底座:
(1) 把底座改为unclickable,保证无法点选
(2) 把底座的碰撞体积改小,保证玩家在框选时只能先选中塔单位 而不会选中底座
当然,这种方法只能让玩家选不中自己的底座。玩家依然是可以选中别的玩家的底座的。
单位能否行走于任意地形(飞行),取决于Elevation level是不是在air层。Elevation level在air层的单位走路会直来直去;在ground层的单位走路会遵循寻路区块,有时会不走直线段。
所有武器相关的事儿都跟elevation level无关,仅跟Advanced Flags里面的bit2(Flying Unit)有关。(bit2是第3个bit: bit0, bit1, bit2, ...)
现给出“空中单位”和“地面单位”的定义:
如果单位A的Advanced Flags里面勾上了Flying Unit,则称单位A为空中单位
如果单位A的Advanced Flags里面没勾Flying Unit,则称单位A为地面单位
一个单位只能是空中单位或者地面单位中的一种。
单位能否被对空武器攻击,取决于它是否为空中单位:
如果单位是空中单位,则它仅可以被对空武器攻击
如果单位是地面单位,则它仅可以被对地武器攻击
每个单位都可以设置对空武器和对地武器,每个武器(130种武器)也都可以作为每个单位的对空武器或者对地武器。但是每个武器都有它自己的target flag
攻击打在目标单位上是否会构成伤害,取决于这个武器的target flag是否和目标单位吻合:
如果打出的武器勾上了air而没勾ground,且目标单位是地面单位,则武器不会对目标单位构成伤害。比如100号武器Neutron Flare,它的target flag里面就只有Air而没有Ground,所以如果把100号武器Neutron Flare作为某个单位A的对地武器,则当目标单位B为地面单位时,单位A可以对单位B发动攻击,但是对单位B无法构成伤害。
单位在create时是否会被挤开仅仅取决于该单位是空中单位还是地面单位:
空中单位在create时会被已经占位的空中单位挤开,地面单位则会被已经占位的地面单位挤开。
此外,造建筑时的绿框只会被地面单位影响,不会被空中单位影响
EE3的requirement的bug:现已修复
以order requirement的119号举例:
Originally, "Default" is selected, capacity = 1316/1316
Step 1: I switch to "User Custom", then capacity = 1320/1316
Step 2: I switch back to "Default", then capacity = 1316/1316
The above is alright.
However, If I compile the project between step 1 and 2, then the capacity cannot go back to normal:
Originally, "Default" is selected, capacity = 1316/1316
Step 1 : switch to "User Custom", then capacity = 1320/1316
Step 1.5 : Compile
Step 2 : switch back to "Default", then capacity = 1320/1316 (bug)
星际重大bug:
把Techdata的14号Dark Swarm的tech use Requirement改成Always Use之后:
假设单位A有一个按钮是放黄雾技能(给出order 119),快捷键是w,此时选中单位A,然后框选B,在B被选中的一瞬间按w,如果时机恰到好处,那么就会出现“单位B被选中的同时,按钮被按下并出现select target黄色⊙光标”,此时左键空地,即可让单位B(有些单位可以,有些单位不行)收到119号order。如果这个单位是兵,收到了119号order,由于他没用放黄雾的动画,所以就会出现一块石头。在塔防图中,由于scv造塔、升级塔、卖塔都需要特定的order来检测,这个bug可能导致塔接收到scv的order从而出现bug。
解决办法:
方法1:
在创建塔的时候把Cannot receive order给set了:
if (twAddr > 0)
SetMemoryXEPD(twEPD + 0xDC/4, SetTo, 0x00001000, 0x00001000); // Cannot receive orders
这个方法的致命缺陷:会导致白球以及英雄白球无法攻击。所以不能用这个办法
方法2:
把Techdata的14号Dark Swarm的tech use Requirement改成Current Unit is SCV,即可防止塔单位接收到黄雾order
注意:把119号order的requirement改成Current unit is SCV是没用的。
技能的释放只看tech use Requirement而不看order requirement
蝎子的黄雾技能最终还是要看Techdata的14号Dark Swarm的tech use requirement,而不是119号order的requirement
一个单位使用技能,确实是 先检测单位的能量值够不够,然后检测tech use requirement,都满足 即可释放技能
如果单位能量不够,会先提示【能量不足】
如果能量足,那么就可以select target(出现黄色光标),玩家选target时需要点击鼠标左键(目标为空地或者某个单位),(鼠标左键空地时先会检测这个技能本身能否作用于空地)然后检测tech use requirement,不满足tech use requirement就会导致左键空地之后什么事都不发生。如果requirement满足,那么鼠标左键之后该单位就会收到对应的释放技能的order
星际的重要特性:
本地指令从“发出”到“共享完成”之间存在延迟,单机游戏这个延迟大约为2帧,多人游戏无延迟条件下这个延迟为3帧,等等。
这个特性会导致一些很难debug的bug出现,以buildcheck举例,有以下两个bug:
(1)单位A有一个按钮用来造兵,假设这个按钮造的兵的unitID为x。使用以下eps代码(错误示范)来实现按钮触发:
const P1SelectedUnitPTR, P1SelectedUnitEPD = cunitepdread_epd(EPD(0x6284E8)); //获取P1所选择的第一个单位所在的地址及EPD
if (BuildCheckEPD(P1SelectedUnitEPD, x)) { // 检测P1所选择的单位正在建造单位x
CreateUnit(1, "Terran Marine", "Anywhere", P1); // 给P1刷一个枪兵
BuildResetEPD(P1SelectedUnitEPD); // 将P1所选择的单位的buildQueue清空
}
游戏开始后,选中单位A,然后在按下单位A的按钮之后迅速选中另一个单位B,这会导致上面的代码无法检测到A在建造x,导致单位A顺利开始建造x(当然也不会刷出枪兵),若单位A本身不是出兵建筑而是men,那么在经过了x的建造时间之后,游戏就直接崩溃。
原因:
假设指令共享的延迟为3帧。选中了单位A,假设在第k帧时按下单位A建造按钮,此时这个建造指令开始被共享,在共享完成(第k+3帧)之前,单位A的buildQueue都是空的,因此buildcheck检测不到任何东西。然后在k+2帧选中了另一个单位B,此时P1SelectedUnit变成了单位B,而单位B没有建造东西,buildcheck也检测不到。然后到了第k+3帧,建造指令共享完毕,单位A接到了建造指令,开始造单位x。但是此时玩家选中的是单位B。因此在整个过程中,buildcheck无法检测到单位A正在建造x。
解决办法:
千万不要用“对玩家所选择的单位进行BuildCheck”这个思路来实现按钮触发。选择其他方式(比如固定某个单位)来实现按钮触发。
(2)单位A有一个按钮用来造兵,假设这个按钮造的兵的unitID为x。我想让这个按钮存在cooldown,即玩家按了一次之后,按钮立即变灰,过一段时间之后才变亮,玩家才能再按,即玩家每次只能按1次按钮。若使用以下eps代码(错误示范)来实现单次按钮触发:
//uepd为某个单位A的地址所对应的epd值
//timer为按钮cooldown计时器
//TurnOnButton()和TurnOffButton()为定义好的两个函数,分别是使按钮变量和变灰,都是在执行的瞬间完成目的
if (timer > 0)
timer--;
if (timer == 1)
TurnOnButton(); // 倒计时结束时让按钮变亮
if (BuildCheckEPD(uepd, x)) { // 检测单位A正在建造x
CreateUnit(1, "Terran Marine", "Anywhere", P1); // 给P1刷一个枪兵
TurnOffButton(); // 让按钮变灰
timer = 100; //将倒计时器的值设为100
BuildResetEPD(uepd); // 将单位A的buildQueue清空
}
则会导致:游戏开始后,选中单位A,然后迅速连续按多次按钮,按钮变灰后,可能会看到刷出了2个甚至3个Terran Marine。
原因:
假设指令共享的延迟为3帧。我选中单位A之后,假设我在第k帧按下按钮,则这个build指令在第k+3帧才会被收到,届时BuildCheck才能检测到单位A造了x并把按钮变灰。也就是说,当我在第k帧按下按钮时,直到第k+3帧按钮才会变灰,因此我可以在第k帧和第k+1帧连续按两次按钮,两次指令分别会在第k+3帧和第k+4帧收到。在k+3帧时,buildcheck生效,并buildreset并且按钮变灰,但虽然在第k+3帧buildreset了,但是在第k+4帧还是收到了第k+1帧的指令,此时buildcheck又会生效,导致CreateUnit。
解决办法:
if (BuildCheckEPD(uepd, x)) { // 检测单位A正在建造x
if (timer == 0) { // 保证在timer为0时才会生效
CreateUnit(1, "Terran Marine", "Anywhere", P1);
TurnOffButton(); // 让按钮变灰
timer = 100; //将倒计时器的值设为100
}
BuildResetEPD(uepd); // BuildReset必须要伴随着BuildCheck,不能写在if(timer == 0)的环境内
}
这样,在第k+4帧虽然BuildCheck生效了,但是此时timer已经从0变成了100,所以CreateUnit不会生效。
游戏内看到的图层跟elevation level有关,重叠的图层,谁的elevation level高就显示谁。比如把坦克的elevation level设为18(坦克炮头不变),则create出来的坦克就看不到炮头。
CreateUnit无法create出P12的unit
埋地的lurker give给P12之后会自动站起来
地图预置的、以及用GiveUnits give给P12的隐形单位(包括埋地单位等),玩家是看不见的
让玩家看到P12的隐形单位,只能使用eud:
SetMemoryX(0x0057F218, SetTo, 0xFF, 0xFF); // Give P12's vision to P1-P8
eps代码对于import的变量:
// module.eps:
var x;
const arr = EUDArray(10);
// main.eps:
import module as m;
m.x++; // 错误!
SetVariables(m.x, 1, list(Add)); // 正确!
m.arr[0]++; // 正确!
SetVariables(m.arr[0], 1, list(Add)); // 错误!
Flingy.dat的Turn Radius是单位在运动过程中转向时的向心力大小,值为0至255。值为0时,游戏崩溃;值为1时,单位只能原地踏步;值较小时,单位无法转向,只能按照原有的朝向走直线。值越大,单位转向时所画的圆弧的半径越小。值为255时,单位可视为万向轮。
我一直说的unitID或许称为unitType更合适。因为unit index代表的是单位在scmd中的摆放顺序,即刚开始游戏时摆放预置单位的顺序
Unit index:
0: 0x59CCA8
1: 0x59CCA8 + 336 * 1699 (Unit Node Table的最后336byte)
2: 0x59CCA8 + 336 * 1698 (Unit Node Table的倒数第二个336byte)
...
1699: 0x59CCA8 + 336 * 1
Unit Alpha ID = 2049 + (unitPTR - 0x59CCA8)/336
关于Hotkeyed Unit: 0x57FE60
Each u32 stores the Alpha ID of a hotkeyed unit:
Starting from 0x57FE60: All the hotkeyed unit of P1
0x57FE60: Alpha ID of the 1st unit in P1 hotkey 0
0x57FE64: Alpha ID of the 2nd unit in P1 hotkey 0
0x57FE68: Alpha ID of the 3rd unit in P1 hotkey 0
...
0x57FE60 + 4 * 11: Alpha ID of the 12th unit in P1 hotkey 0
0x57FE60 + 4 * 12: Alpha ID of the 1st unit in P1 hotkey 1
0x57FE60 + 4 * 13: Alpha ID of the 2nd unit in P1 hotkey 1
...
0x57FE60 + 4 * 119: Alpha ID of the 12th unit in P1 hotkey 9
From 0x57FE60 + 4 * 120 to 0x57FE60 + 4 * 215: ??????
-----------
Starting from 0x57FE60 + 4 * 216: All the hotkeyed unit of P2
-----------
Starting from 0x57FE60 + 4 * 216 * 2: All the hotkeyed unit of P3
-----------
...
-----------
Starting from 0x57FE60 + 4 * 216 * 7: All the hotkeyed unit of P8
-----------
0x57FE60 + 4 * 216 * P + 4 * 12 * H + 4 * i: Alpha ID of the i-th unit in hotkey H of playerID P
Unit node table是一个双向链表(doubly linked list),有主链和支链:
FirstUnitPointer -> Unit1 <-> Unit2 <-> "Terran Siege Tank" <-> Unit4 -> Null
|
"Tank Turret"
主链和支链都会占用内存空间,比如以上的例子中,bring条件会判定总共有4个单位,但是实际上1700个单位空间有5个都被占用了。
EUDLoopUnit()只会loop主链上的单位,因此会无视掉"Tank Turret"
而EUDLoopUnit2()会loop所有占用unit node table空间的单位,因为它不是按照链表的顺序loop的,而是按照内存顺序loop的。
CUnit Node链表的单位创建顺序、内存位置:
定义:单位的MemoryIndex = (unitPTR - 0x59CCA8) / 336 + 1
即 MemoryIndex = AlphaID - 2048 = AlphaID & 0b0111 1111 1111
即 unitPTR = 0x59CCA8 + (MemoryIndex - 1) * 336
单位的MemoryIndex与单位的地址是一一对应的关系,下文使用MemoryIndex来指代单位在内存中的位置
星际创建单位有如下原则:
(1) 从内存角度:
FirstEmptyUnit指针(即0x628438内的指针)起始值为0x59CCA8,即指向的memoryIndex为1,
然后下一个单位被创建在FirstEmptyUnit所指向的地址内,与此同时,FirstEmptyUnit变为[1, 1700, 1699, 1698, 1697, ..., 2]中第一个未被占用的地址。当1700个位置未被占满时,任何单位的死亡都不会影响FirstEmptyUnit的值。当1700个位置全部被占用时,FirstEmptyUnit变为NULL指针,若此时有单位死亡,则FirstEmptyUnit会指向可用的内存。
例:假设此时FirstEmptyUnit为1600,且CUnit链表中被占用的地址为1, 2, 1698, 1699,1700
则下一个单位(Node)会被创建在1600(若此单位有subunit,则其subUnit会被视为创建另一个node,因此其subUnit将位于1697),创建完成后,FirstEmptyUnit变为1697,即地图的下一个单位会被创建在1697,然后FirstEmptyUnit变为1696。若此时位于1700的单位死亡,则FirstEmptyUnit的值仍为1696,不受影响。
(2) 从链表角度:
当链表内只有1个Node时,此Node既为表头、又为表尾,此时创建的新Node为表尾。
当地图内有至少2个单位时,新创建的单位永远位于第1个Node和第2个Node之间,即:
创建第1个unit:
head -> unit1
创建第2个unit:
head -> unit1 -> unit2
创建第3个unit:
head -> unit1 -> unit3 -> unit2
创建第4个unit:
head -> unit1 -> unit4 -> unit3 -> unit2
若此时unit3死亡,则:
head -> unit1 -> unit4 -> unit2
创建新的unit:
head -> unit1 -> unit5 -> unit4 -> unit2
若此时unit1死亡,则:
head -> unit5 -> unit4 -> unit2
创建新的unit:
head -> unit5 -> unit6 -> unit4 -> unit2
综上所述:
假设地图中总共有5个预置单位,其unitIndex分别为0, 1, 2, 3, 4(星际按照unitIndex从小到大依次创建单位),假设这5个单位均只占用1个Node(不存在SubUnit),则当它们都创建完毕时:
链表:unit0 -> unit4 -> unit3 -> unit2 -> unit1
内存: 1, 1697, 1698, 1699, 1700
此时FirstEmptyUnit为1696
注意!Map Revealer不在主链内,而是自成一链。
例:如果unit4,unit6,unit8是map revealer,则:
head -> unit1 -> unit5 -> unit3 -> unit2
maprevealerHead -> unit4 -> unit8 -> unit6
EUDLoopUnit(): 顺着主链loop unit,无法loop到sub unit和map revealer等
EUDLoopUnit2(): 顺着Memory index来loop unit,可以loop到全部unit
但是Map Revealer在PlayerUnit的链中,即EUDLoopPlayerUnit(P1)可以access到P1的Map Revealer
主机名后缀#xA.BC的本质是改变每一帧有多少毫秒,起作用的只有整数部分A,而小数部分BC不起任何作用。
A=0: 每帧42毫秒,原速, wait(42) = wait(2fr)
A=1: 每帧36毫秒,约1.17倍速, wait(42) = wait(3fr)
A=2: 每帧29毫秒,约1.45倍速, wait(42) = wait(3fr)
A=3: 每帧21毫秒,2倍速, wait(42) = wait(3fr)
A=4: 每帧12毫秒,3.5倍速, wait(42) = wait(5fr)
A=5: 每帧1毫秒,42倍速, wait(42) = wait(43fr)
A>=6: 每帧42毫秒,原速
并且当主机名有这个后缀时,SetMemory(0x5124D8 + 4*6, SetTo, 毫秒) 会失效,即游戏会使用#xA所决定的游戏速度,无视0x5124D8 + 4*6这个内存的值。
执行以下eps代码:
function onPluginStart() {
setcurpl(0);
if (IsUserCP()) {
dwread(0);
}
}
后果:
P1报eud错误0xFFFFFFFF,其他玩家显示P1读秒。
关于eps代码的import(在euddraft0935及之前的版本成立)
假设我们有如下文件结构:
D:\eud\euddraft.exe
D:\eud\plugins\MSQC.py
D:\root\main.edd
D:\root\TriggerEditor\main.eps
D:\root\TriggerEditor\data\core.eps
D:\root\TriggerEditor\data\basic.eps
D:\root\src\a.eps
D:\root\other\b.eps
则main.edd中的所有插件只能为绝对路径 或者 相对路径 或者 以euddraft.exe所在路径的plugins文件夹为基的相对路径(这个不太确定)
绝对路径是基于你的计算机的基路径,比如D:\ (这个不太确定)
相对路径是指基于main.edd的相对路径
in "main.edd":
[main]
input: ...
output: ...
[TriggerEditor\main.eps] <-此为相对路径,由于main.edd位于D:\root\,所以此代码所代表的文件(插件)是D:\root\TriggerEditor\main.eps
[other\b.eps] <-此为相对路径,代表的文件(插件)是D:\root\other\b.eps
[MSQC]
QCUnit: 68
对于每一个插件,里面的所有被引用的文件里面的import也可以是“绝对路径”、相对路径:
绝对路径:以main.edd为基
相对路径:以该插件的路径为基
即:import后面写的东西xx.yy.zz,euddraft只会在以下2个地方寻找:
(1) main.edd所在路径\xx\yy\zz.eps
(2) 本plugin所在路径\xx\yy\zz.eps
in "D:\root\TriggerEditor\main.eps":
import data.core as c; // 此为相对于main.eps这个插件的相对路径,指D:\root\TriggerEditor\data\core.eps
import src.a as a; // 此为“绝对路径”,指D:\root\src\a.eps
import other.b as b; // 此为“绝对路径”,指D:\root\other\b.eps
in "D:\root\TriggerEditor\data\core.eps":
import basic; // 错误!因为core.eps本身并不是作为一个插件被写在main.edd中,而是作为被main.eps所引用的文件,所以这种写法会导致euddraft找不到basic.eps文件
import data.basic; // 正确。即使basic.eps和core.eps处于同一目录内,也需要这么写
import TriggerEditor.data.basic; // 正确
注:2021.09.10更新了euddraft0936,以上内容不再适用
SCV飞行bug的本质:SCV的status flag的0x00100000(IsNormal)在建造建筑时是disable掉的,建筑建造完成后会自动enable。通过卡bug可以让这个flag一直disable。
SCV飞行bug要领:
让scv摆下一个建筑,摆下之后按一次ESC让SCV中止建造。此时选中任何一个scv,右键点该未完工的建筑,此时该scv的按钮会变成只有一个ESC的按钮集(如果按钮没变,那就再选中一次该scv),同时scv会走向未完工的建筑。此时按住shift然后狂按ESC键,scv走到未完工建筑之前达到unit waypoint is full,然后在scv上建筑开始建造之后按一次ESC让SCV中止建造,scv就会变成无视地形无视碰撞的状态。
SCMD将单位x的血量设为某个临界值以上就会导致:地图预置单位x在游戏中血量为残血。
这个临界值为2^24/100,即167772.16,即小于等于167772 + (40/256)就正常,大于等于167772 + (41/256)就残血
原因是SCMD默认enable UNIT+14的bit 1 (HP is valid FLAG),就会导致星际认为这个单位的血量是100%,在游戏开始后放置该单位使用的是CreateUnitWithProperties(..., hitpoints=100)而不是CreateUnit(...),导致在计算血量时overflow。
解决办法:
(1) 使用Starforge编辑器。SF编辑器不会默认enable那个flag
(2) 使用scmd保存完地图之后手动修改chk的UNIT section,把相关的flag都disable掉
注:eudplib的fixUnitData函数干的就是这件事情。所以任何地图经过euddraft编译后都不会有这个bug
MSQC的重要知识:
MSQC插件的位置非常重要!
由于MSQC的send()和receive()都位于MSQC.beforeTriggerExec()
所以:
"send"的值的变化应该在在MSQC.beforeTriggerExec()之前
"receive"的值的应用应该在MSQC.beforeTriggerExec()之后
正确例子:把[MSQC]插件位置写在[main.eps]下方,且:
main.before() : value of "send" refreshes;
MSQC.before()
MSQC.after()
main.after() : print send, receive;
-- Time lag = 2 frame
详见:
EUDproj\TestMSQC\MSQCsend2receive_lag
核弹/原子弹 伤害计算:
(x * 2 / 3 * p / 256 - a) * s % 65536
对于幻象单位,需要对原始伤害*2(不太确定*2的位置在哪)
(x * 2 / 3 * 2 * p / 256 - a) * s % 65536
其中:
字母的含义
x:单位的总血量,等于最大盾值+最大生命值的整数部分
p:被攻击单位在溅射圈的位置因子:在溅射圈的内圈时为256,中圈为128,外圈为64
a: 单位的总防御,等于单位的盾防+目前所拥有的盾值+血防
运算符号:
- : 减法运算符
* : 乘法运算符
/ : 整数除法运算符,结果是整数除法得到的商。例:1 / 3的结果为0, 2 / 3的结果为0, 3 / 3的结果为1, 4 / 3的结果为1,以此类推
% : 取余运算符,结果是整数除法得到的余数。例:1 % 3的结果为1, 2 % 3的结果为2, 3 % 3的结果为0, 4 % 3的结果为1,以此类推。该运算符优先级与乘法除法相同,即当没有括号时,连续的乘法、除法、取余运算符的计算顺序为从左向右。
eps:
var x = true;
// 若想toggle x:
x = !x; // 不太好
DoActions(x.AddNumberX(1, 1)); // 正解
CUnit的Status Flag中的0x10000000 - Speed Upgrade这个flag仅对使用iscript.bin movement control的单位有效,即:
以叉叉为例,叉叉的movement control是iscript.bin,所以将某个叉叉的Status Flag的0x10000000设为enable状态时,可以将该叉叉提速。
以人族雷车为例,人族雷车的movement control是flingy.dat,所以将某个雷车的Status Flag的0x10000000设为enable状态并不能让该雷车提速。
关于DotTool,核弹点儿的画图:
DotTool中总共有23种pixel,分为4大类:
(1) 单色(非白色)
(2) 纯白色
(3) 混合色(不含白色)
(4) 混合色(含白色)
可用的Iscript只有89号和250号,分别是Science Vessel(Turret)和Siege Tank(Tank) Turret Overlay
89号的特点:保持image第1帧的图像
250号的特点:image第1帧的图像维持2游戏帧后销毁
当pixel的载体为普通单位(men)时,可以使用89号脚本,则可长时间显示颜色,并可以选中、order等,Remove后即可消失。但是只能使用单色,即(1),因为普通单位不可重叠
当pixel载体为雷达时(SpriteID 380的ImageID改成233):需要先RemoveAll在DrawPixel。优点是不挡路,永远都能Create
- 如果脚本为89,则不能使用任何跟白色有关的pixel,即只能使用(1)和(3)。白色会导致崩溃(即使有Remove触发,也会崩溃)
- 如果脚本为250,则混合色会闪烁,即只能使用(1)和(2)
当pixel载体为door(205, 206号单位)时:也是需要先RemoveAll再DrawPixel。脚本可设为89,优点是可以组成各种颜色(1)(2)(3)(4),缺点是door有碰撞体积,可能会挡路。
Flingy.dat里面:
topSpeed的单位是“像素/(256帧)”
acceleration的单位是“像素/(256帧)^2”
Halt Distance(刹车距离)= topSpeed^2/(2*acceleration),单位就是 (1/256)像素
iscript.bin 结构:
总大小为40482bytes,略小于40KB
其中,最后的1596字节,除去最后4字节FF FF 00 00之后剩下的1592字节是各个iscriptID于其offset的信息表,由2字节iscriptID+2字节offset这样的4字节结构构成,总共储存了398个iscript的offset:
C6 00 FC 1C C7 00 9C 1D C8 00 2C 21 79 01 1A 89 78 01 3A 88 77 01 78 87 ......
首4字节:C6 00 FC 1C 意思是iscript 0x00C6(即198号iscript)信息位于iscript.bin +0x1CFC
以下为iscript.bin +0x1CFC的内容:
53 43 50 45 0C 00 00 00 20 1D 2B 1D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 59 1D 25 1D 00 00 09 53 01 00 00 00 66 00 07 BA 79 18 35 00 34 00 00 99 00 05 01 00 9A 00 05 01 00 9B 00 05 01 00 9C 00 05 01 00 9D 00 05 01 00 9E 00 05 01 00 9F 00 05 01 00 A0 00 05 01 16 29 04 05 01 00 77 00 29 04 05 01 00 88 00 29 04 05 01 00 00 00 29 04 05 01 00 11 00 29 04 05 01 00 22 00 29 04 05 01 00 33 00 29 04 05 01 00 44 00 29 04 05 01 00 55 00 29 04 05 01 00 66 00 07 59 1D 00
首4字节为"SCPE"标识,接下来的4字节为iscript type(此例中type为12),接下来每两字节代表各个animation function所在的offset
[00] Init: 在 0x1D20
[01] Death: 在 0x1D2B
[02] GndAttkInit [NONE]
[03] AirAttkInit [NONE]
[04] Unused1 [NONE]
[05] GndAttkRpt [NONE]
[06] AirAttkRpt [NONE]
[07] CastSpell [NONE]
[08] GndAttkToIdle [NONE]
[09] AirAttkToIdle [NONE]
[10] Unused2 [NONE]
[11] Walking 在 0x1D59
[12] WalkingToIdle 在 0x1D25
[13] SpecialState1 [NONE]
每个iscript都有最多28个animation functions,紧接着type之后的2字节永远代表Init的offset,接下来的2字节永远代表Death的offset。。。
以此iscript的Init function为例:
Init在0x1D20,因此去iscript.bin +0x1D20寻找其OP code:
以下为iscript.bin +0x1D20的内容:
09 53 01 00 00 00 66 00 07 BA 79 18 ......
当执行该Init函数时,会从iscript.bin +0x1D20读取opcode一直执行下去,直到遇到END或者GOTO这两个opcode
如何检测某个人类玩家退出游戏/Defeated:
bread(0x57EEE0 + 36 * p + 0x8) == 10