“Pere工作笔记”的版本间的差异

来自星际争霸重制版地图研究所
跳到导航 跳到搜索
(清空全部内容)
标签可视化编辑 手动回退 清空
 
(未显示同一用户的2个中间版本)
第1行: 第1行:
<nowiki>===========</nowiki>


Statistics:
<nowiki>===========</nowiki>
星际1中攻击距离的判定取决于边缘像素,而不取决于单位的质心。如:坦克射程12的意思是:当坦克与攻击目标处在同一条横线或竖线上时,只要坦克的最边缘像素距离攻击目标最边缘像素的距离小于等于12*32=384像素,则坦克可以攻击到目标,大于384像素则无法攻击到目标。记坦克所占的所有像素集为S1(该集合中共有32*32=1024个像素),攻击目标所占像素集合为S2,则定义坦克与攻击目标之间的距离为d(S1,S2)=min{d(A,B)}(A属于S1,B属于S2)
其中d(A, B)的具体算法并不是直接勾股定理,而是一套比较复杂的近似算法,详见GPTP:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp</nowiki>
getDistanceFast
然而,技能释放距离的判定与攻击不同。以核弹为例,鬼兵发射核弹的射程为9格(升级视野后为11格),这个则以中心距离为准:鬼兵中心所在的位置(即鬼兵的物理坐标)距离核弹发射中心的坐标小于等于9*32=288像素时,可以发射核弹。已实验核弹,未实验其他单位的技能。
原子弹伤害距离判定:这个也是计算的边缘像素距离。原子弹射程为8格,当单位的边缘像素距离原子弹中心的距离<8格,即<8*32=256像素时,才有可能受到伤害。原子弹全伤害半径:4格,溅射伤害为5-8格圆环,其中5-6格圆环为半伤害,7-8格圆环为1/4伤害。注:若多颗核弹重叠,则会出现“落点偏离”现象:即某些核弹的真实落点不在红点上,而是略微偏左或偏右或偏上(经试验,未出现过偏下的情况)。所以,鬼兵的坐标与最远端伤害距离为19格。经计算,宏图中,1点能否用原子弹炸到3点的农民(9点与11点同理)的判定标准为:1点高地下端与3点水晶矿之间如果有18格,则可以炸到农民,若19格则炸不到农民。
All spells except for "Yamato Cannon" and "Nuke" ignore unit armor.
参考 <nowiki>http://www.staredit.net/wiki/index.php?title=Damage_Order_of_Operations</nowiki>
另外关于下面的数据,参考 <nowiki>http://www.staredit.net/wiki/index.php?title=Spells_(Zerg)</nowiki> 等
关于魔法的连续攻击,都有:伤害间隔为8fr=0.5游戏秒=0.336地球秒(约每秒3次伤害)
闪电兵的闪电攻击:一共造成8次伤害,每次伤害为14,总伤害112,无视任何护甲和防御,伤害间隔为8fr=0.5游戏秒=0.336地球秒(约每秒3次伤害),总持续时长为64fr=4游戏秒=2.688地球秒。
辐射:一共造成75次伤害,每次伤害853/256(约3.332),总伤害约为249.9,无视任何护甲和防御。伤害间隔为8fr,总持续时长为600fr=37.5游戏秒=25.2地球秒。
蝎子红血:一共造成75次伤害,每次伤害1010/256(约3.945),总伤害为295.8984375(约295.9),无视任何护甲和防御,伤害间隔为8fr,总持续时长为600fr=37.5游戏秒=25.2地球秒,若单位血量小于等于3.945,则停止伤害。
大和炮:260e
原子弹:中心区域伤害跟自爆人一样,都是500es
地雷:125es
攻击伤害计算原理:
先用防御数来抵消攻击数(若不破防则有效伤害为0.5并且不接着往下算了),然后再根据单位的体型来判断伤害亏损,得出最终伤害值。注:神族单位剩余0盾(实际值u32值为0-255)时,盾防不生效,所有伤害作用于本体。
注:单位显示的生命值 = ceil(单位实际生命值)
例:1攻Ghost打1防大和:
Ghost攻击为10+1=11,大和总防御为3+1=4,计算伤害时候先用防御抵消攻击:11-4=7,得到有效伤害为7,然后由于ghost攻击大型单位攻击变为原来的25%,所以最终Ghost每个攻击给大和的实际伤害为7*25%=1.75血。
例:1攻坦克(架起)打3防狗:
1攻坦克攻击为70+5=75,狗的防御为3,抵消完后有效伤害为72,由于狗是小型单位所以坦克攻击减半,所以最终实际伤害为72/2=36,所以1攻坦克架起来可以秒杀3防狗
例:2辆3攻坦克(架起)同时打3盾0防的叉叉:
先看第一个坦克:3攻坦克攻击为70+5*3=85,叉叉盾防为3,抵消后还剩82,神族护盾为全伤害,因此60点护盾被打完,还剩22伤害,叉叉本体防御自带1,所以还剩21伤害,计算攻击减半效果之后还剩10.5伤害,因此第一个坦克对于叉叉的内血的伤害为10.5,然后看第二个坦克:由于两辆坦克同时攻击,所以叉叉来不及回盾,所以第二辆坦克攻击时不会触发盾防御,因此85点攻击全部作用于叉叉本体,减去自带的1防后还剩84攻击,减半后是42的实际攻击,所以叉叉还剩100-10.5-42=47.5血(游戏界面会显示还剩48血)。
例:虫族自爆人炸一个被上了保护套的0防叉叉
自爆人伤害为500es,即爆炸伤害,攻击小单位伤害减半。保护套250,叉叉盾60,吃掉自爆人的310伤害,还剩190作用于叉叉本体,叉叉本体自带1防,还剩189伤害,减半后是94.5伤害,故炸完后叉叉还剩5.5滴血,显示为6滴血。
建筑的防:
Z的bc坯子和防空都是0防,对地地堡是2防,其他所有建筑都是1防。
另:虫族的蛋是中型单位。暴雪战网数据有误。
例:两颗地雷同时炸0防叉叉(在其身旁爆炸)
第一颗地雷:125-60盾=65,叉叉自带1防,还剩64,叉叉小型单位,减半,对实体伤害为32;第二颗地雷直接对实体进行伤害,抵消掉叉叉的1防还剩124,减半后伤害为62,32+62=94,最后叉叉还剩6滴血。
例:700血、200防的scv,站在原子弹的次级溅射区(1/2伤害区),则被伤害多少?
原子弹在决定是伤害500还是最大生命值的2/3时,仅看最大生命值,不看防御、是否被科学球上了套等等。最大血量大于750,则原子弹就决定伤害最大生命值的2/3,否则伤害500,此情况下,原子弹决定伤害500,次级伤害区是决定伤害250,抵消200防之后,还剩50,scv是小型单位,原子弹伤害减半,最终伤害是25
具体的伤害计算(damage calculation)逻辑见源代码:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/wpnspellhit.cpp</nowiki>
void WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor);
void MeleeAttackHit(CUnit* attacker);
...
得到base_damage(面板伤害)之后,调用damageWith function,计算如下:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_damage.cpp</nowiki>
void weaponDamageHook( s32 damage,
CUnit* target,
u8 weaponId,
CUnit* attacker,
u8 attackingPlayerId,
s8 direction,
u8 dmgDivisor
)
射程:(如未说明,均为计算边缘距离)
scv:10像素
小狗、叉叉、隐刀:15像素
大牛:25像素
地雷:30像素(距离目标30像素处即可引爆,计算边缘距离)
神族农民、虫族农民、火兵:32像素
白球:64像素
地雷pop-up:96像素(计算地雷中心与目标中心距离)
水晶矿的图片根据矿量不同而不同:
0 - 249矿
250 - 499矿
500 - 749矿
750及以上
<nowiki>===========</nowiki>
Map Edit:
<nowiki>===========</nowiki>
关于Mission objective:
游戏开始前的mission objective在trigger - mission briefing里面设置,设置完了之后也可以在游戏开始后在菜单中的mission objective里面看到。
而在trigger - map trigger 里的 (action) “set mission objectives to” 设置之后会重置mission objective
关于use map setting模式是否给初始基地:
在Scenario - Map properties中,只要给玩家的race设置成了user select,就会导致游戏开始时在start location处送给他一个基地和4个农民,然后其他的预设单位全都不给;只要给他的race选了某一个种族(如Zerg),就不会给初始基地和农民,而是给他地图编辑时设定的预设单位。
关于Forces里面的randomize start location
在若干个force中,只要有一个force勾了randomize start location,那么就会导致Map properties里面设定的颜色随机分配给所有玩家
组合AI的试验:Expansion Difficult简称D,insane简称I
D处有基地,I处无基地,先D后I(指AI触发在触发列表中的先后顺序):电脑开局在D处造一个基地后傻掉(不给农民则直接傻)
D处有基地,I处无基地,先I后D:当初始农民送采完1-2轮水晶时,派一个农民去I处造基地,同时在D处造农民、发展。如果开局不给农民或者只给1个农民,则直接傻掉。
D处无基地,I处有基地,先D后I:初始农民送采完1-2轮水晶时,派一个农民去D处造基地,同时在I处造农民。等D处基地造好之后,才在I处放血池。如果开局不给农民或者只给1个农民,则直接傻掉。
D处无基地,I处有基地,先I后D:当初始农民送采完1-2轮水晶时,在I处造基地,同时造农民。农民造出来之后派4个农民去D处远程采矿。之后只在I处发展。如果开局不给农民或者只给1个农民,则直接傻掉。
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则不行。
其实,var基本等价于EUDVariable()
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来改变单位颜色了,同理EPDCUnitMap里的unit.is_dying()也无法使用了
检测是否单人游戏模式 (single player mode):
dwread(0x0057F0B4)=dwread_epd(-11436)==0: single player
dwread(0x0057F0B4)=dwread_epd(-11436)==1: multiplayer
在EE2的触发代码结构下,Preserve trigger(trigger末尾把timer设为0)貌似会使得simpleprint不正常。这种情况要用setcurpl(getuserplayerid())来代替,
注: 这个是euddraft长期以来的bug,与EE2无关。在onPluginStart()中写上GetGlobalStringBuffer()即可解决
4个OB位的playerID:
128  129
130  131
把单位的placement box (0x662860+4*uID)(EUDDB里面说的是building dimensions)改成0,0 可以让建筑变成invisible,改成31,31或以下,则可以让建筑放在任何地方,也可以叠任意数量。可以用这种方法来create叠起来的单位
当把unit X的placement box改成0,0后,地图中现有的X仍然看得见,但是变得不可选中,新create的unit X是不可见的。
若此时再把X的placement box改正常值,则原先不可见的X会变得可见,并且地图中全部X变得可以选中。
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都进行buildQueue[0]的判断,只有buildQueue[0]==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个房子(overlord)。房子的位置跟start location的位置有关。
如果start location位于地图第一象限,则房子在基地左下;
如果start location位于地图第二象限,则房子在基地右下;
如果start location位于地图第三象限,则房子在基地右上;
如果start location位于地图第四象限,则房子在基地左上。
详见 void CreateInitialOverlord(u8 playerId):
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/create_init_units.cpp</nowiki>
对于“事先分号队伍、在游戏中随机点位”的宏图,必须用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的显示是向下取整。
详见代码:
u32 CUnit::getCurrentHpInGame() const {
assert(this);
return (this->hitPoints + 255) >> 8;
}
u32 CUnit::getCurrentShieldsInGame() const {
assert(this);
return this->shields >> 8;
}
各种常数:
Energy魔法回复:+8每帧
虫族血量回复速度:+4每帧(即4/256个血)
神族能量电池能量回复:被加盾的单位+1280每帧
神族能量电池能量消耗:能量电池自己-640每帧
神族shield回复速度:+7每帧(即7/256个盾)
护士加血:+200每帧
护士加血耗能:-100每帧
鬼兵隐形耗能:-18每帧
隐飞隐形耗能:-21每帧
建筑燃烧减血:-20每帧
参考资料:
<nowiki>https://liquipedia.net/starcraft/Game_Speed</nowiki>
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"的情况。
用kill或者remove触发杀掉的单位通通不会算入death数,如果房子里面装着兵,然后用kill/remove触发杀掉房子,则房子里面的兵也不会算入death数。
单位X和Y均为己方单位,X打死Y时,Y不计kill数(己方的Y的kill数不变),但是Y会计入death数(己方的Y的death数+1)。
此外,可自爆的单位(自杀机 Scourge, 自爆人 Infested Terran, 地雷 Spider Mine)在自爆完成攻击时,均不计入death数,仅在被打死时才计入Death数。
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 的组合:
<nowiki>-----</nowiki>
--(A)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(B)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(C)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(D)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(E)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(F)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(G)
<nowiki>-----</nowiki>
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
<nowiki>-----</nowiki>
--(H)
<nowiki>-----</nowiki>
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得来的。它在功能上等价于直接Create一个image
原理:
Create一个scanner sweep之后,它所对应的image的iscript会执行 sprol 380,即create一个 380号sprite (scanner sweep hit),因此如果我们将380号sprite的image改成我们想要的image,就可以随意创建我们想要的image了,并且只要适当设置iscript与drawfunction,我们可以让这个image播放不同的帧、或者改变颜色。
基本步骤:
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的iscript的init anim最好只有 playfram, wait这些opcode,并必须以end结尾。如果有诸如 domissiledmg 的 opcode,则游戏崩溃。
调整imageID为xxx的各种参数,比如IscriptID(如果想播放尽量少的image,则设为250, 因为iscript250仅播放第1帧image并仅持续2游戏帧)和draw func(控制颜色)
如果所选的image的iscript自带的init动画不以end结尾(如果不干预就会永远存在于地图中),则需要手动更改Iscript使得Init动画以end结尾。
然后根据需求决定是否更改image xxx的iscript:
SetMemory(0x66EC48+4*xxx, SetTo, yyy);
下面的帖子总结了所有能用的iscript
<nowiki>https://cafe.naver.com/edac/90187</nowiki>
在下一步是create雷达:
CreateUnit(1, "Scanner Sweep", "loc", player)
如更改了image xxx的iscript,则最好还原:
SetMemory(0x66EC48+4*xxx, SetTo, 原本的iscript);
还原scanner sweep hit的image:
SetMemoryXEPD(EPD(0x666458), SetTo, 546, 0xFFFF), // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546
RemoveUnit(33, player)  // remove掉scanner sweep, 阻止其 iscript再执行 sprol 380生成其他的scanner sweep hit sprite
注: RemoveUnitAt(All, 33, 'loc', player) 并不能成功remove雷达
注: Scanner Sweep Hit 这个 Sprite的 elevation level 是5,因此它会遮住所有 elevation level 为0至4的单位,并且会位于elevation level 为6或以上的单位 的下面。对于elevation level为5的单位,不太确定,貌似是: 如果先create scanner sweep在create单位,则特效盖住单位;反之则特效在单位底下。
以下使用例全部由raw actions组成,因此也可以用scmd直接实现
使用例1: 仅改scanner sweep hit的 imageID
function onPluginStart() {
    SetMemoryX(0x661558, SetTo, 0x20000, 0x20000); //Scanner sweep: can create
    setcurpl(0);
    DoActions(
        SetMemoryX(0x666458, SetTo, 510, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 510: Acid Spore Hit. 此image的 Iscript的 Init动画以end结尾,故播放完毕后自动消失
        CreateUnit(1, "Scanner Sweep", "Anywhere", CurrentPlayer),  // 给P1创建雷达特效
        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546
        RemoveUnit(33, CurrentPlayer),
    );
}
使用例2: 改scanner sweep hit的 imageID,由于Image没有end,因此要改Iscript使其end
function onPluginStart() {
    SetMemoryX(0x661558, SetTo, 0x20000, 0x20000); //Scanner sweep: can create
    setcurpl(0);
    DoActions(
        SetMemoryX(0x666458, SetTo, 450, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 450: Flames1 Type1 (Small). 此image的 Iscript的 Init动画是不断重复的,没有end,如果不更改其Iscript,它将永远存在,无法消失。
        SetMemory(0x66EC48+4*450, SetTo, 34),  // ImageID 450 (Flames1 Type1 (Small)): IscriptID 322->34 (Zerg Building Explosion)
        // Iscript改成34的原因是,450号image总共有0-11共12帧,34号Iscript刚好也是播放前12帧每帧间隔2,然后end
        CreateUnit(1, "Scanner Sweep", "Anywhere", CurrentPlayer),  // 给P1创建雷达特效
        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546
        SetMemory(0x66EC48+4*450, SetTo, 322),  // 为了不影响正常的火焰,要把其Iscript还原成322
        RemoveUnit(33, CurrentPlayer),
    );
}
使用例3: 将Iscript改成250使其仅播放第1帧然后wait2消失,让特效看上去像是跟随着目标单位。Iscript 250是一个重要的Iscript,它几乎可以应用于999个image中的任意一个。 缺点就是只播放image的第一帧动画。若想让一个单位的影子消失,就可以把它的影子的Iscript改成250。下面的例子是让一个 White Circle始终在某个枪兵脚下(假设地图中只有一个枪兵):
function onPluginStart() {
    DoActions(
        SetMemoryX(0x661558, SetTo, 0x20000, 0x20000), //Scanner sweep: can create
        SetMemoryX(0x663150, SetTo, 6, 0xFF), // 将Terran Marine的elevation level设为6
        CreateUnit(1, "Terran Marine", "Anywhere", P1),  
        SetMemoryX(0x663150, SetTo, 4, 0xFF), // 将Terran Marine的elevation level还原为4
    );
}
function beforeTriggerExec() {
    setcurpl(0);
    DoActions(
        SetMemoryX(0x666458, SetTo, 503, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 503: White Circle. 此image的 Iscript运行时会导致游戏崩溃,所以必改Iscript
        SetMemory(0x66EC48+4*503, SetTo, 250),  // ImageID 503 (White Circle): IscriptID 292->250 (仅播放第1帧动画,并持续2游戏帧),因此枪兵走动时会有1帧的尾巴。如果不想有尾巴,可以隔一帧创建一次,但是看上去会很难受,因为不连续。
        MoveLocation("loc1", "Terran Marine", CurrentPlayer, "Anywhere"),
        CreateUnit(1, "Scanner Sweep", "loc1", CurrentPlayer),  // 给P1创建雷达特效
        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546
        SetMemory(0x66EC48+4*503, SetTo, 292),  // 把其Iscript还原成292
        RemoveUnit(33, CurrentPlayer),
    );
}
若不想让雷达特效拥有正常的雷达功能,还可以:
SetMemoryX(0x664080 + 33 * 4, SetTo, 0, 0x8000)  // 取消雷达的Detector属性
SetMemoryX(0x663258, SetTo, 0, 0xFF00)  // 让雷达视野为0,但是这个貌似并不影响create雷达后雷达开视野
关于DotTool,核弹点儿的画图:
DotTool中总共有23种pixel,分为4大类:
(1) 单色(非白色)
(2) 纯白色
(3) 混合色(不含白色)
(4) 混合色(含白色)
可用的Iscript只有89号和250号,分别是Science Vessel(Turret)和Siege Tank(Tank) Turret Overlay
89号的特点:保持image第1帧的图像,通常以unit为载体
250号的特点:image第1帧的图像维持2游戏帧后销毁,通常以scanner sweep hit (sprite)为载体
当pixel的载体为普通单位(men)时,可以使用89号脚本(此仅播放1帧, frame 0), 因此则可长时间显示颜色,并可以选中、order等,Remove后即可消失。但是只能使用单色,即(1),因为普通单位不可重叠
当pixel载体为雷达 scanner sweep 时(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有碰撞体积,可能会挡路。
改变单位影子的Iscript来让其他image接在单位的下方:
原理: 将影子的Iscript改成有imgul/imgol opcode的Iscript (比如311号Iscript Zerg Beacon),这样即可让单位下面有一个创建出来的image,主要要让这个image变成unclickable,不然单击会选中单位。
使用例:
function onPluginStart() {
    setcurpl(0);
    DoActions(
        SetMemory(0x66EC48+4*1, SetTo, 211),  // ImageID 1 (Scourge shadow): IscriptID 275->211 (Zerg Beacon)
        SetMemoryX(0x66C150+355/4 * 4, SetTo, 0<<(355%4 * 8), 0xFF << (355%4 * 8)),  // ImageID 355 (Zerg Beacon Overlay) SetTo 0: unclickable
        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 下方有Zerg Beacon Overlay, 且点击Zerg Beacon Overlay不能选中单位
        SetMemoryX(0x66C150+355/4 * 4, SetTo, 1<<(355%4 * 8), 0xFF << (355%4 * 8)),  // ImageID 355 (Zerg Beacon Overlay) SetTo 1: clickable
        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 下方有Zerg Beacon Overlay, 且点击Zerg Beacon Overlay能选中单位
        SetMemory(0x66EC48+4*1, SetTo, 275),  //  ImageID 1 (Scourge shadow): IscriptID 211->275 (Shadow Header) 还原
        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 正常 Zerg Scourge
    );
}
如何弄出图像为White Circle的单位:
把目标单位的Sprite的imageID改成503 White Circle, 然后把white circle的image设成clickable, Iscript改成 Walking仅有1帧的 Iscript,比如各种空军的Iscript(注意把影子的Iscript设为250),或者金甲子弹的Iscript, 或者260 (ScienceVessel)
0x669E28 Draw function:
0: Normal draw
9: Use Remapping (没用)
10: Shadow
13: 纯绿色
15: Show Rect Size 绿色矩形
16: Hallucination
RemoveUnitAt和KillUnitAt的重大bug:
"loc"为任意一个location,对任意正整数x,有:
KillUnitAt(x, "Terran SCV", "loc", P1);
如果这条Action生效并杀到了任意一个在"loc"内已装机的SCV,则"loc"内所有的P1的已装机的SCV都会被杀掉/清除。
其他可装机的单位同理,其他玩家同理,RemoveUnitAt同理。
造兵时,自己取消造的兵,退全款;出兵建筑被打掉,也退全款。
升级科技时,自己取消升级,退全款;升级建筑被打掉,退75%钱。
关于Z基地菌毯蔓延问题:
编辑器内使用地形或者建筑围住基地,可以使菌毯不蔓延。但是,如果在基地的固有菌毯范围内放置虫族建筑,则会导致菌毯无视地形、无视建筑蔓延开来。
奇怪的现象:
只要触发列表里面有一个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
(在0.9.5.6及之后版本,euddraft默认使用utf8来解码scmd中设置的字符串,因此不再需要加上面这一行。但是,若希望强行使用韩文cp949来解码,则需要手动设置:decodeUnitName : cp949)
关于字符串的编码:
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版本之后,不再需要写py_str,也不再需要手动写\u2009:
settbl("Terran Marine", 0, "简体中文", encoding="utf-8");
因此,如果想将stat_txt.tbl的某一项改为简体中文(以将Terran Marine的tbl text改为"简体中文"为例),完整步骤是:
如果使用的是EE3,则可以直接打开Data Editor - Text,选择 Terran Marine,直接输入"简体中文",保存并编译即可。EE3在编码tbl时,会优先使用cp949编码,若无法被cp949编码,则会使用utf8编码,并自动在字符串末尾添加\u2009字符,详见EE3在编译工程后自动创建的temp\custom_txt.tbl
注: 此为EE3于2021-02-14更新的0.12.3.5版本的新特性,详见:
<nowiki>https://github.com/Buizz/EUD-Editor-3/issues/36</nowiki>
如果你使用的是EE2:
(1) 计算出"简体中文"四个字使用utf8编码后占用的字节数,算出来是12字节。因此真正在tbl中写入的字节为:这12个字节,再加上\u2009\x00这4个字节,总共16字节
python代码:
tbl_str = "简体中文"
bts = tbl_str.encode('utf-8')
n = len(bts)
print(bts)
print(n)
print("<0>" * (n+4))
(2) 在File Manager中打开tbl列表,将"Terran Marine"的text用至少16个<00>来填充,即:
<0><0><0><0><0><0><0><0><0><0><0><0><0><0><0><0>
(3) 写代码 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。
所以,在纯eps工程中,当借用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 (conditionlist中总共16个conditions)的逻辑为:
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为变量(EUDVariable)时,尽量使用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查看即可
教程:
<nowiki>https://cafe.naver.com/edac/63490</nowiki>
如何让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, buildQueue1=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)方法1: build check法
需要选择一个单位作为按钮的载体,这个单位最好选择men单位,而非建筑,具体原因之后讲。此例中以Terran Marine为按钮载体
EE3 - data edit - Button set - Terran Marine - 激活 button list - 右键 - New code - Train Unit - 随便选择一个value作为被造单位(最好选择英雄Unit或者地图中不会用到的unit,假设选择的unit是Jim Raynor枪兵),然后随便选一个图标和注释, Button Work的Condition可以改成3(Always),若需要控制按钮的变亮、变灰,则根据需要来自行定义condition。
然后data edit - Unit - Jim Raynor要修改两个:
default: Mineral和Vespene都改成0
Requirements: Always Use,或者Current Unit is "Terran Marine" (按钮载体单位)
使用变量储存按钮载体单位的ptr/epd地址,然后就可以使用buildcheck和buildreset了
建议使用二模蛤写的BuildCheckConst和BuildResetEPD
示例代码:
if (BuildCheckConst(uepd, 20)) {  // 检测到按钮载体正在建造Jim Raynor (Marine)
    // 执行你想要的功能
    BuildResetEPD(uepd);
}
注意,玩家按下按钮与星际接受到按钮被按下的指令之间有一定的延迟,所以写代码时一定要注意,BuildCheckConst(uepd, 20) 是接受到按钮被按下的指令才会变为True, 而不是玩家按下按钮的时刻
(2)方法2: order法
同样也需要一个单位作为按钮载体,也需要一个“按下即生效”的order作为辅助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... "按钮载体单位"
第二种目的: 按下按钮并选定目标之后触发某功能
Tech-order法:
需要一个order作为辅助order,这个order可以选择没有任何限制的tech(既可作用于空地,也可作用于单位),比如黄雾(Dark Swarm, orderID=119),海盗分裂网(Disruption Web, orderID=181),皇后绿雾(Ensnare, orderID=146)等等。当然,诸如Rally Point (39, 40), patrol (152) 这些可以作用在空地及单位上的order,也可考虑使用,只不过可能要改order的requirement。
下面以SCV作为按钮载体,以order119作为辅助order:
EE3 - data edit - Techdata - [014] Dark Swarm - Usage requirement: 改为 Current Unit is... SCV,或者在is not burrowed之前加两个opcode: "or" "Current Unit is... SCV"
Techdata - [014] Dark Swarm - Default: 消耗能量改为0
(由于order 119 的requirement本来就是alway use, 所以不需要再更改order requirement)
EE3 - data edit - Buttonset - [007] Terran SCV - 把攻击、修理、采矿等无关按钮都删除,然后把蝎子的黄雾按钮给复制粘贴进去
EE3 - data edit - images - [247] SCV - IscriptID 设为含有CastSpell脚本的单位, 比如369 (Corsair)或者 89(Science Vessel Turret)等
注意,为了保证不让载体单位真的把技能释放出来,我们要在代码中检测order时把载体单位的order设为3,这个就需要Iscript的 CastSpell动画中的 sigorder 2 opcode 之前至少有1个wait opcode,以保证单位不会瞬发技能。比如红球的iscript 925和926 就都是瞬发技能,在这里不能使用。
Iscript 369的缺点是释放技能时有分裂网的音效。比较推荐Iscript 89。
代码示例:
const u = EPDCUnitMap(uepd);
const orderTarget_ptr, orderTarget_epd = u.orderTarget;
const tgtX, tgtY = u.getpos("orderTargetPosition");
if (u.orderID == 119) {
    u.orderID = 3;  // 还原orderID,保证每次按按钮仅触发一次
    MoveUnit(1, "Terran SCV", P1, "loc1", "loc1");  // 如果目标离scv很远,则即使把orderID设为3了scv还是会自动走过去,所以需要手动让它停止。"loc1"是一直黏在scv上的location
    const tgt_ptr, tgt_epd = cunitepdread_epd(orderTarget_ptr);
    if (tgt_ptr > 0) {  // 检测到以某单位为order目标
        // 实现相应功能
    }
    else {  // 检测到以空地目标
        // 实现相应功能
    }
}
在使用按钮触发的时候应注意,如果按钮是在建筑上,那么点按钮之后这个建筑或许就真的会造兵(新开辟内存),如果这个按钮在一些men上,那么点按钮之后就不会开辟新内存。
EE2自带的BuildReset function不够严谨
CUnit里面有一个currentBuildUnit (+0xEC)
让任何一个工厂或者你按钮触发的母单位去造一个unit的时候,这个正在被建造的单位都会占用1700个单位中的一个单位,其地址储存在母单位的CUnit的currentBuildUnit中
实际游戏过程中,当我们按ESC取消建造的时候,星际其实做了两件事:
(1) 把母单位(工厂)的buildQueue[buildQueueSlot]设为228
(2) 给currentBuildUnit所指向的单位一个death order,杀掉它(或者直接free这个336字节的内存)
而EE的BuildReset只做了第一件事儿,而没有做第二件事
如果不做第二件事,就会导致Command AtMost可以检测到这个正在被建造的单位。
深入探究按钮造兵的本质:
当按钮的Action为Train Unit (Create Unit)时,按按钮成功之后会发出指令: CMDACT_Train
若干游戏帧后,星际接受到这个指令,即 train_cmd_receive,并调用函数 CMDRECV_Train(u16 wUnitType):
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/recv_commands/train_cmd_receive.cpp</nowiki>
此函数会call HasMoneyCanMake(builder, wUnitType),这个函数 add unit to build queue if successful
然后会setSecondaryOrder(OrderId::Train)
另外,secondaryOrdersRoot(CUnit* unit) 会不断检测unit的secondOrder:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/0_orders/orders_root.cpp</nowiki>
当second order为OrderId::Train时,call function_00468420(CUnit* unit)
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/unit_making/unit_train.cpp</nowiki>
此函数中, (可能是当当前单位为建筑物时?)会执行attemptTrainHatchUnit (在内存中新建CUnit),并会call IscriptAnimation::IsWorking
所以如果想用让建筑来造兵,必须要保证该建筑有IsWorking动画
<nowiki>--------</nowiki>
如何让任意兵造建筑:
以“让Marine 造防空塔”为例:
(1) Buttonset - Terran Marine - 把239号buttonset的防空塔button给复制粘贴过去
(2) Orders - [30]Build - Requiments - 改成Always或者在scv的基础上再加上 Or Current Unit is "Terran Marine" (注意capacity!)
(3) Units - [124]Terran Missile Turret - Requiments: 加上 Or Current Unit is "Terran Marine"
原理:
按钮的Condition是 4(can create Unit/Buiding) 124(Terran Missile Turret)
因此第3步保证了按钮是按钮是亮的?且能按下按钮?
在按下按钮并放下建筑后,会发送一个command,然后:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/recv_commands/CMDRECV_Build.cpp</nowiki>
收到button的command之后,会执行 CMDRECV_PlaceBuildingAllowed 函数检查是否能建造:
第3步保证了 canMakeUnit(builtUnitId, *ACTIVE_NATION_ID) 返回true
第2步保证了 OrderAllowed(builder, orderId, *ACTIVE_NATION_ID) 返回true
注意,当同时选中两个兵时,右下角的按钮面板会变为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
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的CUnit->status (status flag) 有InAir flag,则称单位A为空中单位
如果单位A的CUnit->status (status flag) 没有InAir flag,则称单位A为空中单位
注:若使用触发将units.dat里的单位A的Advanced Flags里面勾上Flying Unit,则再此之后Create的新的A都是空中单位,但地图中现有的单位A皆不受影响。
同理,若使用触发将units.dat里的单位A的Advanced Flags里面uncheck Flying Unit,则再此之后Create的新的A都是地面。
由此可得,一个单位只能是空中单位或者地面单位中的一种。且在地图中同时存在的若干个同一种单位,可能有的是空中单位,有的是地面单位。
一个有武器的单位A,是否会去尝试攻击单位B,取决于单位A所装备的武器(空或地,或都有)是否与目标单位B的空地属性所匹配,即:
若单位A有对空武器(即它的air weapon不是130),且单位B是空中单位,则单位A会尝试攻击单位B。注:此处用“尝试”,意思是之后还要走其他逻辑流程。
若单位A没有对空武器(即它的air weapon是130),且单位B是空中单位,则单位A不会攻击B。
同理,如果单位A有 Ground weapon,且单位B是地面单位,则A会尝试攻击B
如果单位A没有 Ground weapon,且单位B是地面单位,则A不会攻击B
每个单位都可以设置对空武器和对地武器,每个武器(130种武器)也都可以作为每个单位的对空武器或者对地武器。
武器打在目标单位上是否会构成伤害,取决于这个武器的weapon Effect (或称为 explosion type) 以及 target flag是否和目标单位吻合,这里比较复杂
当武器的weapon Effect为air splash时:
如果打出的武器勾上了air而没勾ground,且目标单位是地面单位,则武器不会对目标单位构成伤害。比如100号武器Neutron Flare,它的target flag里面就只有Air而没有Ground,所以如果把100号武器Neutron Flare作为某个单位A的对地武器,则当目标单位B为地面单位时,单位A可以对单位B发动攻击,但是对单位B无法构成伤害。
当武器的weapon Effect为default时:
武器的target flag不管勾不勾ground或air,都会打出伤害(待求证)
上面说的“尝试攻击”,是指单位A的Iscript的对应的function会被调用。
如果单位A有对空武器,尝试攻击空中单位B,则此时单位A的会被给到一个Attack order (10),这个order会调用该单位的image的Iscript的AirAttkInit(或者AirAttkRpt) Animation函数
如果单位A有对地武器,则若地面单位B在A的seek range之内、武器射程之内时,A则会尝试攻击B。此时单位A会被给到AIorder的AttackUnit order,默认情况下为order  10,这个order会调用该单位的image的Iscript的GndAttkInit(或者GndAttkRpt)
orders.dat里面有Animation这一项,含义为Iscript的animation函数,如果值为28,则说明该order所调用的 animation function 是 hardcode的,不随这一项设置的值变化。比如order 10就是 hardcode的,永远调用GndAttkInit。
具体只有以下16种order可以通过更改orders.dat里面的Animation项来改变该order所调用的iscript animation function:
FireYamatoGun1, MagnaPulse (lockdown), DarkSwarm, CastParasite, SummonBroodlings, EmpShockwave, PsiStorm, Irradiate, Plague, Consume, Ensnare, StasisField, Restoration, CastDisruptionWeb, CastOpticalFlare, CastMaelstrom
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/spells/cast_order.cpp</nowiki>
以枪兵攻击空中单位为例,枪兵尝试攻击空中单位时,AirAttkInit会被调用,而枪兵的AirAttkInit函数是MarineGndAttkInit,即:
MarineGndAttkInit:
playfram        0x00 # frame set 0
wait            1
playfram        0x11 # frame set 1
wait            1
playfram        0x22 # frame set 2
MarineGndAttkRpt:
wait            1
nobrkcodestart
playsnd        69 # Bullet\TMaFir00.wav
attackwith      1
playfram        0x33 # frame set 3
wait            1
playfram        0x22 # frame set 2
wait            1
playfram        0x33 # frame set 3
wait            1
playfram        0x22 # frame set 2
wait            1
playfram        0x33 # frame set 3
wait            1
playfram        0x22 # frame set 2
wait            1
nobrkcodeend   
gotorepeatattk
ignorerest     
MarineGndAttkToIdle:
playfram        0x11 # frame set 1
wait            1
playfram        0x00 # frame set 0
wait            1
goto            MarineWalkingToIdle
以上脚本执行到attackwith 1时,才会真正创建一个子弹。
注:attackwith 1的含义是“使用该单位的 ground weapon进行攻击”,如果数字是除了1以外的数,则代表用air weapon进行攻击。
所以说,无论你把枪兵的air weapon改成什么,只要枪兵有air weapon,那么它都会使用ground weapon来攻击
创建一个子弹flingy,此时238号Iscript的Init被执行,执行到domissiledmg命令时,才会真正对被攻击的目标造成伤害。
具体逻辑详见星际代码:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp#L96-L135</nowiki>
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/base_orders/attack_orders.cpp</nowiki>
举例:
当A为Terran Marine、B为Terran Medic、w为Gauss Rifle时:
A仅有对地武器w(将对空武器设为130),单位B为地面单位(未勾Flying Unit),无论w勾Air/勾Ground/都勾/都不勾,都是: A可攻击B,并有伤害
A仅有对地武器w,单位B为空中单位(勾了Flying Unit),无论w勾Air/勾Ground/都勾/都不勾,都是: A不能攻击B
A仅有对空武器w(将对地武器设为130),单位B为地面单位,无论w勾Air/勾Ground/都勾/都不勾,都是: A不能攻击B
A仅有对空武器w,单位B为空中单位,无论w勾Air/勾Ground/都勾/都不勾,都是: A可攻击B,但是没有伤害,且会造成overflow,导致出现不可预测的现象(会随着星际的小版本的改变或星际32bit/64bit而改变),比如出现石头,或者游戏崩溃,
第4种情况的详细解释:
枪兵攻击护士时,枪兵的Iscript的AirAttkRpt或GndAttkRpt)函数会执行,当然,具体call谁并不重要,因为两个都指向MarineGndAttkRpt函数。
MarineGndAttkRpt函数中有"attackwith 1"脚本,这个脚本会让枪兵的 Group Weapon生效("attackwith 2"就是让Air Weapon生效)。由于Ground Weapon是130,所以星际会读取 Weapon.dat里面每一项的+4*130 offset,这个是典型的读取溢出,会读到不可预测的内容,也就会读取到不可预测的Graphic,因此可能会出现石头,也可能导致其他不可预测的事情。
关于星际1处理单位攻击的底层逻辑(各种武器、子弹、伤害的底层逻辑):
实际情况较为复杂,需要具体问题具体分析。
例1: 雷车攻击
当雷车选定了攻击目标(某地面单位)并准备攻击时,其orderID为10,此时它的 iscript(id=86) - AttkGround 被call:
VultureGndAttkInit:
    wait            1
    attackwith      1
    gotorepeatattk
    goto            VultureGndAttkToIdle
当 attackwith 1 被执行时(1代表对地武器),函数 CUnit::fireWeapon(u8 weaponId) 被call,即位于0x00479C90的 fireWeaponHook(CUnit* unit, u8 weaponId) 被call,其中 weaponId为雷车的对地武器:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_fire.cpp</nowiki>
fireWeapon这个函数一般会被attackwith, attack, castspell这几个opcode call
这个函数会执行: createBullet(unit, weaponId, x, y, unit->playerId, unit->currentDirection1)
此时会生成一个CBullet. CBullet是一个类似于CUnit的struct,详见:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/CBullet.h</nowiki>
createBullet函数的具体内容不明。但可以推测,这个函数会获取weaponID的信息,比如behavior/effect等,并决定是否要寻找unit->orderTarget,即attacker的目标CUnit的地址。还有这个bullet是否会miss (CBullet::hitFlags),等等,并将这些信息写入被Create出来的CBullet中。(所以说,一个武器是否会miss,是在它生成的时候就定下来的,而不是它打到目标上时)
之后,这个CBullet的image的iscript的Init被call,以雷车的 Fragmentation Grenade 为例:
它的init被call时所执行的opcode并不会造成任何伤害。成功create之后,该Fragmentation Grenade会根据自己的CBullet信息来飞向目标单位,并在到达指定地点时call iscript的 death anim (整个过程应该是由某个未知函数所控制的):
imgol          440 0 0 # Fragmentationgrenadehit (thingy\efgHit.grp)
domissiledmg   
wait            1
end           
其中domissiledmg才是真正对目标单位造成伤害的opcode。 它的底层逻辑是:
opcode domissiledmg 会call这个函数: weaponImpact(CBullet* bullet):
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_impact.cpp</nowiki>
这个函数会根据weapon的effect来决定执行的效果,以 Fragmentation Grenade为例,其effect为WeaponEffect::NormalHit,因此进行简单逻辑检查后,若没miss的话就call这个函数:
WeaponBulletHit_Helper(bullet, target, damage_divisor), 即位于0x00479AE0的 WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor):
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/wpnspellhit.cpp</nowiki>
target->damageWith(damage, weaponId, attacker, attacker->playerId, attacker->currentDirection1, 1):
即 CUnit::damageWith(s32 damage, u8 weaponId, CUnit* attacker, u8 attackingPlayer, s8 direction, u8 damageDivisor):
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/CUnit.cpp</nowiki>
次函数又会call Func_DoWeaponDamage, 即:
weaponDamageHook(s32 damage, CUnit* target, u8 weaponId, CUnit* attacker, u8 attackingPlayerId, s8 direction, u8 dmgDivisor)
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_damage.cpp</nowiki>
这个函数才是真正对目标造成伤害的函数,执行我们所熟悉的伤害值计算的逻辑。
计算出伤害结果之后,还会call CUnit::damageHp函数,此函数又会call killTargetUnitCheck 来检查被攻击的target单位是否死亡
本例为 iscript中opcode为 attackwith时的一个特定例子。
简单来讲,iscript的attackwith会生成一个CBullet,此CBullet在death时会执行iscript的domissiledmg,并对目标造成伤害。
小知识: domissiledmg只能作用于CBullet,若作用于CUnit则游戏崩溃
例2: 架起来的坦克的攻击
与雷车相似,但是子弹有区别,坦克的子弹是直接生成的target上,并且init anim会直接执行到death,并执行domissiledmg。
之后在 weaponImpact(CBullet* bullet)中,由于arclite cannon shot的effect是 splash around target (WeaponEffect::SplashRadial),
故会执行到 FindBestUnit(&area_of_effect, Func_SplashProc, bullet)
这个函数会对范围内的每个单位都call WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor),并计算伤害。
FIndBestUnit函数的具体内容不明,疑似会调用 bool canWeaponTargetUnit(u8 weaponId, CUnit* target, CUnit* attacker) 函数:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp</nowiki>
如何让科学球称为可以对空对地攻击的单位:
给科学球加对地、对空武器
改Acquisition range
AI-order RC: 1
Iscript设成frame不多的、有对空对地攻击的、有castspell的,比如66号(battle cruiser)
若嫌shadow烦,则可把shadow的draw function变成隐形,或者iscript变成直接death的
单位在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
星际的重要特性:
本地指令(game command)从“发出”到“共享完成”之间存在延迟,单机游戏这个延迟大约为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,注:当x为常数而非EUDVariable时,应该用BuildCheckConst函数
    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不会生效。所以这样才能保证,在按钮处于“亮”状态时,无论玩家以多快的速度连续按多少次按钮,都只能触发1次 Create枪兵
游戏内看到的图层跟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++;  // 错误! 从euddraft0970开始,此写法正确
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-based index):
0: 0x59CCA8
1: 0x59CCA8 + 336 * 1699 (Unit Node Table的最后336byte)
2: 0x59CCA8 + 336 * 1698 (Unit Node Table的倒数第二个336byte)
...
1699: 0x59CCA8 + 336 * 1
alphaID (一个u16数据)的定义:
Unit Alpha ID = (uniquenessIdentifier << 11) | index = (uniquenessIdentifier << 11) + index
uniquenessIdentifier: 某个CUnit内存被CUnit占用的次数, 即CUnit +0xA5所储存的u8数据
index: 即1-based index: (unitPTR - 0x59CCA8)/336 + 1
<nowiki>https://github.com/armoha/euddraft/issues/93</nowiki>
alphaID的低11位是CUnit的1-based index,高5位是uniquenessIdentifier的低5位
当uniquenessIdentifier=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: ??????
<nowiki>-----------</nowiki>
Starting from 0x57FE60 + 4 * 216: All the hotkeyed unit of P2
<nowiki>-----------</nowiki>
Starting from 0x57FE60 + 4 * 216 * 2: All the hotkeyed unit of P3
<nowiki>-----------</nowiki>
...
<nowiki>-----------</nowiki>
Starting from 0x57FE60 + 4 * 216 * 7: All the hotkeyed unit of P8
<nowiki>-----------</nowiki>
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, 即CUnit的1-based index
即 MemoryIndex = AlphaID & 0b0111 1111 1111 即AlphaID的低11bit
即 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
注:单位进出运输机,会导致该单位在链表上的相对位置变动,貌似会被移到表头,但该单位所占用的内存地址不变。
Armo — 2022/09/23
Unload Transport/Bunker/Interceptor, Workers Exit Gas Buildings etc re-link unit on top of linked list
Both newly created unit and unloaded unit link to first unit pointer so for EUDLoopNewUnit I had to differentiate them using CUnit+0xA5 uniquenessIdentifier
综上所述:
假设地图中总共有5个预置单位(无任何unit sprite),其unitIndex分别为0, 1, 2, 3, 4(SCMD使用unitIndex这个概念来表示单位被摆下的先后顺序),假设这5个单位均只占用1个Node(不存在SubUnit),则当它们都创建完毕时:
链表:unit1 -> unit5 -> unit4 -> unit3 -> unit2
内存:  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
另外注意,星际优先加载预置的unit sprite,然后再加载预置的units
主机名后缀#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这个内存的值。
如何禁止玩家使用#xN后缀(#x suffix)
执行以下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
使得核弹造成伤害的脚本是:
image 316 (NuclearMissile) 的iscript (131) 的SpecialState1.
详见GPTP:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/spells/nuke_orders.cpp</nowiki>
current_image->playIscriptAnim(IscriptAnimation::SpecialState1);
核弹/原子弹 伤害计算:
(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: 单位的总防御,等于单位的盾防+目前所拥有的盾值+血防
s: 根据武器类型与被攻击单位体型所得的 multiplier
运算符号:
- : 减法运算符
<nowiki>*</nowiki> : 乘法运算符
/ : 整数除法运算符,结果是整数除法得到的商。例: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状态并不能让该雷车提速。
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为0x0C,即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)
紧接着iscript 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(0x16)或者GOTO(0x07)这两个opcode
因此,这些OPCode翻译过来是:
imgul          339 0 0 # RagnasaurShad (neutral\nacShad.grp)
playfram        0x66 # frame set 6 (本质是frame index 0x66)
goto            RagnasaurLocal00
其中:
OPcode 09即imgul, 53 01 00 00 即0x153, 0, 0, 即在(0,0)处的下方图层创建339号image
OPcode 00: playfram, 66 00: 0x66
OPcode 07: goto, BA 79: offset+0x79BA
其中 0x79BA的字节是:
05 7D 07 BA 79
wait 125
goto +0x79BA
无限循环
注意,WalkingToIdle在 +0x1D25,也就是从Init的第6字节开始的
所以我们可以看到,Init相当于先执行了09 53 01 00 00这5个字节之后,开始进入WalkingToIdle的内容
再看Death,从+0x1D2B开始的字节是:
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
playsnd        53 # Misc\CRITTERS\LCrDth00.wav
setfldirect    0
playfram        0x99 # frame set 9
wait            1
playfram        0x9a # frame set 9
wait            1
playfram        0x9b # frame set 9
wait            1
playfram        0x9c # frame set 9
wait            1
playfram        0x9d # frame set 9
wait            1
playfram        0x9e # frame set 9
wait            1
playfram        0x9f # frame set 9
wait            1
playfram        0xa0 # frame set 9
wait            1
end   
注:
[18]: playsnd, [35 00]: 53
[34]: setfldirect, [00]: 0
[00]: playfram, [99 00]: 0x99
[05]: wait, [01]: 1
[00]: playfram, [9A 00]: 0x9A
...
[16]: end
OPcode及其含义的对照表见:
iscript_codes.txt
注意,当遇到无意义的OPcode(比如0xE6, 0x97, 0x54, 0x45等等),星际都会直接无视(即nop),然后接着读下一个字节。
所以,如果在魔改时,某个iscript function被call了之后发现它所在的地址位于iscript.bin的前1368字节中的某一个字节,就会导致一直[00 00 00]即playfram 0,其他都是无效字节,然后读到 +0x560 (即+1368)处,读到了:
0F 00 00 00 84 05 20 79 20 79 20 79 00 00 ...
[0F]: sprol <SpriteID> <SByte x> <SByte y>
所以它所做的事情就是 spawn a sprite with ID 0x0000 at offset position (0, 0x84) 即(0, -125) relative to the current overlay.
而第0号sprite是石头。这也就是为何在读到缺失的iscript脚本时,会在单位的正上方125像素处出现石头:
比如上面198号iscript因某些原因被调用了GndAttkInit函数,而GndAttkInit函数所对应的索引信息是00 00,所以sc就会跳到iscript.bin的最开头去读取opcode,也就出现了刚刚所讲的情况。
重点讲一下 playfram这个opcode
假设目前的脚本时 playfram N, 则:
当该image的 GraphicTurns=0 (未开启状态)时,playfram N读取的就是其GRP的第N帧(从0开始记)
当该image的 GraphicTurns=1 (开启状态)时,playfram N读取的就是其GRP的第N+offset帧, offset取值范围是0-16
其中 offset由当前CUnit/CBullet的 direction (CUnit+0x21)决定,以下是对应表:
direction, frameIndex(offset)
0  -  3:  0
4  - 11:  1
12 - 19:  2
...
116-123: 15
124-131: 16
132-139: 15 (mirrored)
140-147: 14 (mirrored)
...
244-251:  1 (mirrored)
252-255:  0 (mirrored)
举例:
当人族SCV执行了Iscript指令 playfram 0x11 的时候:
假设它此时的direction是17, 则查表可得offset为2, 所以此时播放的是scv.grp的 0x11+2 = 0x13,即编号为19的帧
假设它此时的direction是140, 则查表可得offset为14 (mirror), 所以此时播放的是scv.grp的 0x11+14 = 0x1F,即编号为31的帧的镜像图片
所以说,在星际的默认设置下,凡是有GraphicTurns的image,其对应GRP所包含的图片数都是17的倍数,且以17帧为一组
貌似只有elevation level >= 12的men才能通过修改CUnit+0x21(CurrentDirection)来改变其图片,具体还需更多试验
如何检测某个人类玩家退出游戏/Defeated:
在active player structure里面:
bread(0x57EEE0 + 36 * p + 0x8) == 10
MemoryXEPD(EPD(0x57EEE0) + 2 + 9*p, Exactly, 10, 0xFF)
若是电脑玩家被defeat,则是11
单位攻击的优先级
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_priority.cpp</nowiki>
详见getAttackPriorityHook(CUnit* target, CUnit* attacker)
cammove:
<nowiki>https://cafe.naver.com/edac/78006</nowiki>
cammove的功能:
让你的屏幕(视角)一直跟随某一个特定的location,类似于不断CenterView,但是视角不会瞬间切过去,而是会以一定的加速度、最大速度逐渐移动过去。
cammove使用方法
注: 带有尖括号"<>"的内容是你的自定义内容
步骤:
(1) 在地图中创建一个名为"cammoveLoc"的location, 并且不能在任何触发中使用这个location。此location是cammove插件的辅助location,它将被插件以location名称识别,因此只能命名为"cammoveLoc",但其locationID无关紧要。
(2) 将任意一个switch命名为"cammove"。同理,插件会以名称识别该switch,故它的switchID不重要。此switch为cammove的开关,当此switch的状态为Set时,cammove插件便会自动执行如下功能:
让你的屏幕一直跟随某个特定的location(即下一步中你所设定的location)。当此开关的状态为Cleared时,cammove插件不生效。
(3) 在edd文件中写:
[camMove]
targetloc: <你想要让屏幕跟随的location>
inertia: <某个整数>
maxspeed: <某个整数>
其中,inertia为屏幕移动的惯性(加速度?),maxspeed为屏幕移动的最大速度
(4) 必须加[eudTurbo]!
(5) 多人游戏模式下才可生效
动了任何跟按钮相关的内存(0x5187E8的s_button Offset及其之前的button info)之后,会导致在游戏中同时选中机枪+火兵(或英雄机枪火兵)时不显示兴奋剂按钮。
解决办法是在新单位出现时手动修改其CUnit的 currentButtonSet, 使得它们的buttonset统一为某个单位的buttonset:
function beforeTriggerExec() {
    foreach(ptr, epd: EUDLoopNewUnit()) {
        const u = EPDCUnitMap(epd);
        if (u.unitType == $U("Terran Firebat") ||
            u.unitType == $U("Jim Raynor (Marine)") ||
            u.unitType == $U("Gui Montag")
        ) {
            u.currentButtonSet = $U("Terran Marine");
        }
    }
}
<nowiki>https://cafe.naver.com/edac/85055</nowiki>
Armo - 2022-09-27 16:40
To put it more accurately, unchecking 'Do not become guard' in EE2 AI tab will crash SC when it dies, owned by Computer, and its type is overlord, larva or workers.
神族农民放下建筑时的音效:
protoss\shuttle\pshbld00.wav
神族建筑造好时的音效:
protoss\shuttle\pshbld03.wav
塔防图必备技巧,让普通men单位模拟防御塔,即单位无法移动,但可以通过鼠标右键来自选攻击目标:
(1) Units (以Terran Vulture为例) - AI Order - Unit Attacks: 改成 19 (Tower), RClick Order: 改成 3
(2) Orders - [019] - Requirements: Always Use
(3)* 若塔可能被攻击,则需要uncheck掉advanced - Special Ability Flags的 Full Auto-attack
但是以上只适用于普通men,不适用于金甲和航母。金甲和航母需要特殊处理
当无法手动选择目标时,SC根据如下代码来操纵单位的attack priority攻击优先级:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_priority.cpp</nowiki>
让Ion Cannon可以像普通防御塔一样攻击:
(1) 对空对地武器设为 30(Yamato Gun)
(2) Seek range 设为非0。注意,如果武器射程比seek range短,则单位可能会在自动获取目标之后发呆不攻击(因为射程不够)
(3) AI order设为跟photon cannon一样
(4) 30号weapon的attack angle改成128
(5) Ion Cannon的Iscript改成120 (Missile Turret (Turret))
(6) 若想实现右键手动选择目标,则需要设置order 19的 requirement: Always Use 或者 加一个Ion Cannon
(7) Button Set 改成162 (Photon cannon的buttonset)
注意,Iscript120的death动画是直接end,所以并不会有爆炸动画,效果像是RemoveUnit
让小狗可以攻击空中单位(本质是如何让iscript的attackmelee指令可以攻击空中单位):
(1) 给小狗添加任意对空武器: 目的是让小狗可以尝试攻击空中单位
(2) 小狗的AI order的attackUnit改成任意一个animation为castspell魔法order,比如115 (鬼兵锁定技能)
(3) 将 order115的 select target 改成一个可对空和对地的武器,比如35号武器(小狗自己的武器),然后把武器的对空flag给勾上
(4) 将 order115的 energy改成 44(None): 即不需要消耗魔法。等价地,也可以把115所对应的技能(鬼兵锁定技能)的魔法消耗设为0
(5) 将 order115的 animation 改成 2(Ground attack default),目的是让小狗获得order115之后,call iscript的GndAttkInit function而不是castspell、也不是AirAttk
(6)* 注意该order是否有requirement,若有,则要加上小狗,或者索性设为always
(7)* 做完以上所有之后,小狗可以自动攻击空中单位1次,但是无法自动连续攻击,需要手动操作攻击(因为order属于cast spell,使得单位无法连续攻击)。若想让小狗连续攻击空中单位,需要用触发手动更改小狗的CUnit order等
(8)* 注意将小狗的 seek range 改成大于武器射程的值
以上这样做的后果是,右键敌人后小狗并不攻击,必须主动A,且射程、cooldown、对空对地flag等等是由order115的 select target所选的武器而决定的,但伤害却是小狗对地武器的伤害。并且当小狗站在敌人空中单位正下方并H住时,小狗会调用普通对空攻击,并调用AirAttk,出现石头
让航母小飞机interceptor不出仓,顶在航母母体上攻击:
把intercepter flingy(41号)的movement control改成2 (iscript.bin)
Top vs Bottom (TvB 上对下)模式,固定每个玩家的颜色:
在scmd中,让任何一个玩家使用自定义颜色,即可。
虫族基地与虫卵 hatchery and larva:
虫族基地CUnit里面并没有任何指针用来记录其所连接larva,但是larva里面有指针记录它所连接的基地。
虫族基地决定是否生产新的larva,是由larvaCounter结果是否<3决定的。larvaCounter函数会在基地周围的区域loop所有的larva,如果larva的connectUnit是本基地,则larvaCount+1,详见 secondaryOrd_SpawningLarva:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/larva_creep_spawn.cpp</nowiki>
选中虫族基地后按S,星际所做的事情也是loop基地周围的区域,找到所有connectedUnit是本基地的larva,然后选中它们。
如果使用MoveUnit将larva移走,则这些larva的connectUnit仍是原来的基地,不会变,并会因为order的原因自动往它原来的基地爬。
如果使用MoveUnit将基地走,则它本来的larva都会失去连接,开始乱跑。对于失去连接的larva,可以用如下触发来使其不乱跑:
RunAIScriptAt("Set Generic Command Target", "loc1");
RunAIScriptAt("Make These Units Patrol", "loc1");
具体功能存疑,待试验
units.dat 的 movement flags 0x660FC8的每个bit的含义:
0x01: can move?
0x02:
0x04: flyer?
0x08:
0x10:
0x20:
0x40: can move?
0x80: hovering?
根据units.dat推测:
所有不能动的,都是1个flag都没有
所有只能在地面动的,都有0x01, 0x40
所有地面悬浮的,都有0x01, 0x40, 0x80
所有能飞的(包括人族建筑), 都有上面提到的全部4个flags
注意,CUnit中也有movementFlags,也是1字节,但与units.dat中的movement flags (+0x20)含义不同,见:
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/Cunitlayout.h</nowiki>
关于attack cooldown:
Bool32 attackApplyCooldown(CUnit* unit)
<nowiki>https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_and_cooldown.cpp</nowiki>
此函数被inject到 0x00478B40
GPTP中,别的函数call它时call的是 attackApplyCooldown_Helper
注意,这个函数是在单位处于持续攻击状态之下被call的,因此当cooldown为0时,call的是AttkRpt函数而不是AttkInit。
而如果单位是从Idle状态转变为攻击状态,(尚未在GPTP中找到对应的函数)则命令单位攻击时,单位会先收到order 10,然后站着发呆,直到它的cooldown变成0,然后瞬间call AttkInit,同时将cooldown设为cooldown+rand({-1, 0, 1, 2})
但是枪兵等单位的AttkInit需要执行若干帧后才会进入执行真正的attack with (或其他attack)opcode,如果在执行到attack opcode之前单位被叫停(按了stop,或者单位被remove/death等),则单位就在未开火的情况下,cooldown还被设为最大值了。所以检测单位的cooldown突变,并不100%意味着单位成功开火
无论单位处于什么状态,它的cunit cooldown都会每帧减小1,直到0
当单位处于持续攻击状态时,由于星际内部代码会在cooldown减小到0时让单位开火,并把cooldown设为其武器cooldown+rand({-1, 0, 1, 2}),然后call AttkRpt
故当单位处于持续攻击状态时,用eps代码检测cunit cooldown时无法检测到0,只能检测到cooldown降到1,然后下一帧就升高到cooldown+rand({-1, 0, 1, 2})
检测某特定单位(CUnit)开火的瞬间:
方法(1): 检测cooldown
每帧都读取该CUnit的cooldown (以对地武器cooldown为例) 储存其上一帧的cooldown (记作cd_last),那么它开火的瞬间满足以下充要条件:
u.gCooldown > 0 && cd_last <= 1
方法(2): 检测cooldown
方法2不如1完美,不能检测开火的瞬间,但却能与每次开火对应: 检测gCooldown==weaponCooldown-1的瞬间
检测某类单位开火的瞬间:
以雷车为,想要检测全图中所有雷车的开火瞬间,可以在weapons.dat中将雷车武器的cooldown +100,然后在eps中loop全部雷车:
if (u.gCooldown >= 100) {  // 检测到雷车开火瞬间
    u.gCooldown -= 100;
    // 做其他事情,比如创建特效等
}
以上的方法本质上都是检测cooldown而已,并不代表单位真的开火了。所以如果想用这种方法检测单位开火,则要保证单位的武器是瞬发,即AttkInit脚本在wait1后直接执行 attack opcode,没有前摇。
方法(3): 检测nonbreakable doe start:
检测 status flag (CUnit+0xDC) 的 0x2000, NoBrkCodeStart (Iscript)
因为很多单位的AttkInit animation都有NoBrkCodeStart, 这是比检测cooldown 更精确的方式。
if (u.orderID==10 && u.check_status_flag(0x2000)) {
    检测到开火瞬间
}
所有能释放魔法(cast spell)的单位总结(很难定义什么叫能释放魔法,下面根据不同定义进行列举)
拥有CastSpell动画且CastSpell里面有castspell opcode的 (castspell opcode之后必有 sigorder 2):
Zerg Defiler (46, 52): ImageID=13, IscriptID=9
Infested Kerrigan: ImageID=33, IscriptID=20
Zerg Queen (45, 49): ImageID=46, IscriptID=27
Terran Ghost (1, 99, 100, 104): ImageID=228, IscriptID=70
Sarah Kerrigan: ImageID=237, IscriptID=77
Science Vessel: ImageID=260, IscriptID=88
Protoss Arbiter (71, 86): ImageID=130, IscriptID=146
Dark Templar (Hero): ImageID=129, IscriptID=152
Protoss High Templar (67, 79, 87): ImageID=126, IscriptID=158
Terran Medic: ImageID=944, IscriptID=360
Protoss Dark Archon: ImageID=925 (Dark Archon Energy), IscriptID=365
<nowiki>*</nowiki>Protoss Dark Archon: ImageID=926 (Dark Archon Being), IscriptID=366
Protoss Corsair (60, 98): ImageID=929, IscriptID=369
Protoss Dark Templar: ImageID=933, IscriptID=372
拥有CastSpell动画但是里面没有castspell opcode的:
IscriptID=66: ImageID=218, unitID: Terran Battle Cruiser (4 kinds: 12, 27, 28, 29, 102)
IscriptID=89: ImageID=261 (Science Vessel Turret)
IscriptID=290 (CastSpell=Death): imageID=120(Shuttle Glow), imageID=114(Carrier Glow)
IscriptID=291 (CastSpell=Death): imageID=132, 220, 225, 249, 941, 942, 943
IscriptID=370 (CastSpell=Death): imageID=931 (Corsair Overlay)
含有castspell opcode但并不是位于CastSpell动画的:
Spider Mine: ImageID=258, IscriptID=87, SpecialState1
Nuclear Missile, ImageID=316, IscriptID=131, SpecialState1
<nowiki>*</nowiki>ImageID=543 (Yamato Gun Overlay), IscriptID=314, Init
0x6283F0: usedUnitCount: CUnit occupancy, number of CUnits taken up

2022年11月16日 (三) 23:01的最新版本

===========

Statistics:

===========

星际1中攻击距离的判定取决于边缘像素,而不取决于单位的质心。如:坦克射程12的意思是:当坦克与攻击目标处在同一条横线或竖线上时,只要坦克的最边缘像素距离攻击目标最边缘像素的距离小于等于12*32=384像素,则坦克可以攻击到目标,大于384像素则无法攻击到目标。记坦克所占的所有像素集为S1(该集合中共有32*32=1024个像素),攻击目标所占像素集合为S2,则定义坦克与攻击目标之间的距离为d(S1,S2)=min{d(A,B)}(A属于S1,B属于S2)

其中d(A, B)的具体算法并不是直接勾股定理,而是一套比较复杂的近似算法,详见GPTP:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp

getDistanceFast

然而,技能释放距离的判定与攻击不同。以核弹为例,鬼兵发射核弹的射程为9格(升级视野后为11格),这个则以中心距离为准:鬼兵中心所在的位置(即鬼兵的物理坐标)距离核弹发射中心的坐标小于等于9*32=288像素时,可以发射核弹。已实验核弹,未实验其他单位的技能。

原子弹伤害距离判定:这个也是计算的边缘像素距离。原子弹射程为8格,当单位的边缘像素距离原子弹中心的距离<8格,即<8*32=256像素时,才有可能受到伤害。原子弹全伤害半径:4格,溅射伤害为5-8格圆环,其中5-6格圆环为半伤害,7-8格圆环为1/4伤害。注:若多颗核弹重叠,则会出现“落点偏离”现象:即某些核弹的真实落点不在红点上,而是略微偏左或偏右或偏上(经试验,未出现过偏下的情况)。所以,鬼兵的坐标与最远端伤害距离为19格。经计算,宏图中,1点能否用原子弹炸到3点的农民(9点与11点同理)的判定标准为:1点高地下端与3点水晶矿之间如果有18格,则可以炸到农民,若19格则炸不到农民。

All spells except for "Yamato Cannon" and "Nuke" ignore unit armor.

参考 http://www.staredit.net/wiki/index.php?title=Damage_Order_of_Operations

另外关于下面的数据,参考 http://www.staredit.net/wiki/index.php?title=Spells_(Zerg) 等

关于魔法的连续攻击,都有:伤害间隔为8fr=0.5游戏秒=0.336地球秒(约每秒3次伤害)

闪电兵的闪电攻击:一共造成8次伤害,每次伤害为14,总伤害112,无视任何护甲和防御,伤害间隔为8fr=0.5游戏秒=0.336地球秒(约每秒3次伤害),总持续时长为64fr=4游戏秒=2.688地球秒。

辐射:一共造成75次伤害,每次伤害853/256(约3.332),总伤害约为249.9,无视任何护甲和防御。伤害间隔为8fr,总持续时长为600fr=37.5游戏秒=25.2地球秒。

蝎子红血:一共造成75次伤害,每次伤害1010/256(约3.945),总伤害为295.8984375(约295.9),无视任何护甲和防御,伤害间隔为8fr,总持续时长为600fr=37.5游戏秒=25.2地球秒,若单位血量小于等于3.945,则停止伤害。

大和炮:260e

原子弹:中心区域伤害跟自爆人一样,都是500es

地雷:125es

攻击伤害计算原理:

先用防御数来抵消攻击数(若不破防则有效伤害为0.5并且不接着往下算了),然后再根据单位的体型来判断伤害亏损,得出最终伤害值。注:神族单位剩余0盾(实际值u32值为0-255)时,盾防不生效,所有伤害作用于本体。

注:单位显示的生命值 = ceil(单位实际生命值)

例:1攻Ghost打1防大和:

Ghost攻击为10+1=11,大和总防御为3+1=4,计算伤害时候先用防御抵消攻击:11-4=7,得到有效伤害为7,然后由于ghost攻击大型单位攻击变为原来的25%,所以最终Ghost每个攻击给大和的实际伤害为7*25%=1.75血。

例:1攻坦克(架起)打3防狗:

1攻坦克攻击为70+5=75,狗的防御为3,抵消完后有效伤害为72,由于狗是小型单位所以坦克攻击减半,所以最终实际伤害为72/2=36,所以1攻坦克架起来可以秒杀3防狗

例:2辆3攻坦克(架起)同时打3盾0防的叉叉:

先看第一个坦克:3攻坦克攻击为70+5*3=85,叉叉盾防为3,抵消后还剩82,神族护盾为全伤害,因此60点护盾被打完,还剩22伤害,叉叉本体防御自带1,所以还剩21伤害,计算攻击减半效果之后还剩10.5伤害,因此第一个坦克对于叉叉的内血的伤害为10.5,然后看第二个坦克:由于两辆坦克同时攻击,所以叉叉来不及回盾,所以第二辆坦克攻击时不会触发盾防御,因此85点攻击全部作用于叉叉本体,减去自带的1防后还剩84攻击,减半后是42的实际攻击,所以叉叉还剩100-10.5-42=47.5血(游戏界面会显示还剩48血)。

例:虫族自爆人炸一个被上了保护套的0防叉叉

自爆人伤害为500es,即爆炸伤害,攻击小单位伤害减半。保护套250,叉叉盾60,吃掉自爆人的310伤害,还剩190作用于叉叉本体,叉叉本体自带1防,还剩189伤害,减半后是94.5伤害,故炸完后叉叉还剩5.5滴血,显示为6滴血。

建筑的防:

Z的bc坯子和防空都是0防,对地地堡是2防,其他所有建筑都是1防。

另:虫族的蛋是中型单位。暴雪战网数据有误。

例:两颗地雷同时炸0防叉叉(在其身旁爆炸)

第一颗地雷:125-60盾=65,叉叉自带1防,还剩64,叉叉小型单位,减半,对实体伤害为32;第二颗地雷直接对实体进行伤害,抵消掉叉叉的1防还剩124,减半后伤害为62,32+62=94,最后叉叉还剩6滴血。

例:700血、200防的scv,站在原子弹的次级溅射区(1/2伤害区),则被伤害多少?

原子弹在决定是伤害500还是最大生命值的2/3时,仅看最大生命值,不看防御、是否被科学球上了套等等。最大血量大于750,则原子弹就决定伤害最大生命值的2/3,否则伤害500,此情况下,原子弹决定伤害500,次级伤害区是决定伤害250,抵消200防之后,还剩50,scv是小型单位,原子弹伤害减半,最终伤害是25

具体的伤害计算(damage calculation)逻辑见源代码:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/wpnspellhit.cpp

void WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor);

void MeleeAttackHit(CUnit* attacker);

...

得到base_damage(面板伤害)之后,调用damageWith function,计算如下:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_damage.cpp

void weaponDamageHook( s32 damage,

CUnit* target,

u8 weaponId,

CUnit* attacker,

u8 attackingPlayerId,

s8 direction,

u8 dmgDivisor

)

射程:(如未说明,均为计算边缘距离)

scv:10像素

小狗、叉叉、隐刀:15像素

大牛:25像素

地雷:30像素(距离目标30像素处即可引爆,计算边缘距离)

神族农民、虫族农民、火兵:32像素

白球:64像素

地雷pop-up:96像素(计算地雷中心与目标中心距离)

水晶矿的图片根据矿量不同而不同:

0 - 249矿

250 - 499矿

500 - 749矿

750及以上

===========

Map Edit:

===========

关于Mission objective:

游戏开始前的mission objective在trigger - mission briefing里面设置,设置完了之后也可以在游戏开始后在菜单中的mission objective里面看到。

而在trigger - map trigger 里的 (action) “set mission objectives to” 设置之后会重置mission objective

关于use map setting模式是否给初始基地:

在Scenario - Map properties中,只要给玩家的race设置成了user select,就会导致游戏开始时在start location处送给他一个基地和4个农民,然后其他的预设单位全都不给;只要给他的race选了某一个种族(如Zerg),就不会给初始基地和农民,而是给他地图编辑时设定的预设单位。

关于Forces里面的randomize start location

在若干个force中,只要有一个force勾了randomize start location,那么就会导致Map properties里面设定的颜色随机分配给所有玩家

组合AI的试验:Expansion Difficult简称D,insane简称I

D处有基地,I处无基地,先D后I(指AI触发在触发列表中的先后顺序):电脑开局在D处造一个基地后傻掉(不给农民则直接傻)

D处有基地,I处无基地,先I后D:当初始农民送采完1-2轮水晶时,派一个农民去I处造基地,同时在D处造农民、发展。如果开局不给农民或者只给1个农民,则直接傻掉。

D处无基地,I处有基地,先D后I:初始农民送采完1-2轮水晶时,派一个农民去D处造基地,同时在I处造农民。等D处基地造好之后,才在I处放血池。如果开局不给农民或者只给1个农民,则直接傻掉。

D处无基地,I处有基地,先I后D:当初始农民送采完1-2轮水晶时,在I处造基地,同时造农民。农民造出来之后派4个农民去D处远程采矿。之后只在I处发展。如果开局不给农民或者只给1个农民,则直接傻掉。

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则不行。

其实,var基本等价于EUDVariable()

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来改变单位颜色了,同理EPDCUnitMap里的unit.is_dying()也无法使用了

检测是否单人游戏模式 (single player mode):

dwread(0x0057F0B4)=dwread_epd(-11436)==0: single player

dwread(0x0057F0B4)=dwread_epd(-11436)==1: multiplayer

在EE2的触发代码结构下,Preserve trigger(trigger末尾把timer设为0)貌似会使得simpleprint不正常。这种情况要用setcurpl(getuserplayerid())来代替,

注: 这个是euddraft长期以来的bug,与EE2无关。在onPluginStart()中写上GetGlobalStringBuffer()即可解决

4个OB位的playerID:

128  129

130  131

把单位的placement box (0x662860+4*uID)(EUDDB里面说的是building dimensions)改成0,0 可以让建筑变成invisible,改成31,31或以下,则可以让建筑放在任何地方,也可以叠任意数量。可以用这种方法来create叠起来的单位

当把unit X的placement box改成0,0后,地图中现有的X仍然看得见,但是变得不可选中,新create的unit X是不可见的。

若此时再把X的placement box改正常值,则原先不可见的X会变得可见,并且地图中全部X变得可以选中。

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都进行buildQueue[0]的判断,只有buildQueue[0]==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个房子(overlord)。房子的位置跟start location的位置有关。

如果start location位于地图第一象限,则房子在基地左下;

如果start location位于地图第二象限,则房子在基地右下;

如果start location位于地图第三象限,则房子在基地右上;

如果start location位于地图第四象限,则房子在基地左上。

详见 void CreateInitialOverlord(u8 playerId):

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/create_init_units.cpp

对于“事先分号队伍、在游戏中随机点位”的宏图,必须用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的显示是向下取整。

详见代码:

u32 CUnit::getCurrentHpInGame() const {

assert(this);

return (this->hitPoints + 255) >> 8;

}

u32 CUnit::getCurrentShieldsInGame() const {

assert(this);

return this->shields >> 8;

}

各种常数:

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"的情况。

用kill或者remove触发杀掉的单位通通不会算入death数,如果房子里面装着兵,然后用kill/remove触发杀掉房子,则房子里面的兵也不会算入death数。

单位X和Y均为己方单位,X打死Y时,Y不计kill数(己方的Y的kill数不变),但是Y会计入death数(己方的Y的death数+1)。

此外,可自爆的单位(自杀机 Scourge, 自爆人 Infested Terran, 地雷 Spider Mine)在自爆完成攻击时,均不计入death数,仅在被打死时才计入Death数。

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得来的。它在功能上等价于直接Create一个image

原理:

Create一个scanner sweep之后,它所对应的image的iscript会执行 sprol 380,即create一个 380号sprite (scanner sweep hit),因此如果我们将380号sprite的image改成我们想要的image,就可以随意创建我们想要的image了,并且只要适当设置iscript与drawfunction,我们可以让这个image播放不同的帧、或者改变颜色。

基本步骤:

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的iscript的init anim最好只有 playfram, wait这些opcode,并必须以end结尾。如果有诸如 domissiledmg 的 opcode,则游戏崩溃。

调整imageID为xxx的各种参数,比如IscriptID(如果想播放尽量少的image,则设为250, 因为iscript250仅播放第1帧image并仅持续2游戏帧)和draw func(控制颜色)

如果所选的image的iscript自带的init动画不以end结尾(如果不干预就会永远存在于地图中),则需要手动更改Iscript使得Init动画以end结尾。

然后根据需求决定是否更改image xxx的iscript:

SetMemory(0x66EC48+4*xxx, SetTo, yyy);

下面的帖子总结了所有能用的iscript

https://cafe.naver.com/edac/90187

在下一步是create雷达:

CreateUnit(1, "Scanner Sweep", "loc", player)

如更改了image xxx的iscript,则最好还原:

SetMemory(0x66EC48+4*xxx, SetTo, 原本的iscript);

还原scanner sweep hit的image:

SetMemoryXEPD(EPD(0x666458), SetTo, 546, 0xFFFF), // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546

RemoveUnit(33, player)  // remove掉scanner sweep, 阻止其 iscript再执行 sprol 380生成其他的scanner sweep hit sprite

注: RemoveUnitAt(All, 33, 'loc', player) 并不能成功remove雷达

注: Scanner Sweep Hit 这个 Sprite的 elevation level 是5,因此它会遮住所有 elevation level 为0至4的单位,并且会位于elevation level 为6或以上的单位 的下面。对于elevation level为5的单位,不太确定,貌似是: 如果先create scanner sweep在create单位,则特效盖住单位;反之则特效在单位底下。

以下使用例全部由raw actions组成,因此也可以用scmd直接实现

使用例1: 仅改scanner sweep hit的 imageID

function onPluginStart() {

    SetMemoryX(0x661558, SetTo, 0x20000, 0x20000); //Scanner sweep: can create

    setcurpl(0);

    DoActions(

        SetMemoryX(0x666458, SetTo, 510, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 510: Acid Spore Hit. 此image的 Iscript的 Init动画以end结尾,故播放完毕后自动消失

        CreateUnit(1, "Scanner Sweep", "Anywhere", CurrentPlayer),  // 给P1创建雷达特效

        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546

        RemoveUnit(33, CurrentPlayer),

    );

}

使用例2: 改scanner sweep hit的 imageID,由于Image没有end,因此要改Iscript使其end

function onPluginStart() {

    SetMemoryX(0x661558, SetTo, 0x20000, 0x20000); //Scanner sweep: can create

    setcurpl(0);

    DoActions(

        SetMemoryX(0x666458, SetTo, 450, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 450: Flames1 Type1 (Small). 此image的 Iscript的 Init动画是不断重复的,没有end,如果不更改其Iscript,它将永远存在,无法消失。

        SetMemory(0x66EC48+4*450, SetTo, 34),  // ImageID 450 (Flames1 Type1 (Small)): IscriptID 322->34 (Zerg Building Explosion)

        // Iscript改成34的原因是,450号image总共有0-11共12帧,34号Iscript刚好也是播放前12帧每帧间隔2,然后end

        CreateUnit(1, "Scanner Sweep", "Anywhere", CurrentPlayer),  // 给P1创建雷达特效

        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546

        SetMemory(0x66EC48+4*450, SetTo, 322),  // 为了不影响正常的火焰,要把其Iscript还原成322

        RemoveUnit(33, CurrentPlayer),

    );

}

使用例3: 将Iscript改成250使其仅播放第1帧然后wait2消失,让特效看上去像是跟随着目标单位。Iscript 250是一个重要的Iscript,它几乎可以应用于999个image中的任意一个。 缺点就是只播放image的第一帧动画。若想让一个单位的影子消失,就可以把它的影子的Iscript改成250。下面的例子是让一个 White Circle始终在某个枪兵脚下(假设地图中只有一个枪兵):

function onPluginStart() {

    DoActions(

        SetMemoryX(0x661558, SetTo, 0x20000, 0x20000), //Scanner sweep: can create

        SetMemoryX(0x663150, SetTo, 6, 0xFF), // 将Terran Marine的elevation level设为6

        CreateUnit(1, "Terran Marine", "Anywhere", P1),  

        SetMemoryX(0x663150, SetTo, 4, 0xFF), // 将Terran Marine的elevation level还原为4

    );

}

function beforeTriggerExec() {

    setcurpl(0);

    DoActions(

        SetMemoryX(0x666458, SetTo, 503, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): imageID SetTo 503: White Circle. 此image的 Iscript运行时会导致游戏崩溃,所以必改Iscript

        SetMemory(0x66EC48+4*503, SetTo, 250),  // ImageID 503 (White Circle): IscriptID 292->250 (仅播放第1帧动画,并持续2游戏帧),因此枪兵走动时会有1帧的尾巴。如果不想有尾巴,可以隔一帧创建一次,但是看上去会很难受,因为不连续。

        MoveLocation("loc1", "Terran Marine", CurrentPlayer, "Anywhere"),

        CreateUnit(1, "Scanner Sweep", "loc1", CurrentPlayer),  // 给P1创建雷达特效

        SetMemoryX(0x666458, SetTo, 546, 0xFFFF),  // SpriteID 380 (Scanner Sweep Hit): Restore imageID to 546

        SetMemory(0x66EC48+4*503, SetTo, 292),  // 把其Iscript还原成292

        RemoveUnit(33, CurrentPlayer),

    );

}

若不想让雷达特效拥有正常的雷达功能,还可以:

SetMemoryX(0x664080 + 33 * 4, SetTo, 0, 0x8000)  // 取消雷达的Detector属性

SetMemoryX(0x663258, SetTo, 0, 0xFF00)  // 让雷达视野为0,但是这个貌似并不影响create雷达后雷达开视野

关于DotTool,核弹点儿的画图:

DotTool中总共有23种pixel,分为4大类:

(1) 单色(非白色)

(2) 纯白色

(3) 混合色(不含白色)

(4) 混合色(含白色)

可用的Iscript只有89号和250号,分别是Science Vessel(Turret)和Siege Tank(Tank) Turret Overlay

89号的特点:保持image第1帧的图像,通常以unit为载体

250号的特点:image第1帧的图像维持2游戏帧后销毁,通常以scanner sweep hit (sprite)为载体

当pixel的载体为普通单位(men)时,可以使用89号脚本(此仅播放1帧, frame 0), 因此则可长时间显示颜色,并可以选中、order等,Remove后即可消失。但是只能使用单色,即(1),因为普通单位不可重叠

当pixel载体为雷达 scanner sweep 时(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有碰撞体积,可能会挡路。

改变单位影子的Iscript来让其他image接在单位的下方:

原理: 将影子的Iscript改成有imgul/imgol opcode的Iscript (比如311号Iscript Zerg Beacon),这样即可让单位下面有一个创建出来的image,主要要让这个image变成unclickable,不然单击会选中单位。

使用例:

function onPluginStart() {

    setcurpl(0);

    DoActions(

        SetMemory(0x66EC48+4*1, SetTo, 211),  // ImageID 1 (Scourge shadow): IscriptID 275->211 (Zerg Beacon)

        SetMemoryX(0x66C150+355/4 * 4, SetTo, 0<<(355%4 * 8), 0xFF << (355%4 * 8)),  // ImageID 355 (Zerg Beacon Overlay) SetTo 0: unclickable

        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 下方有Zerg Beacon Overlay, 且点击Zerg Beacon Overlay不能选中单位

        SetMemoryX(0x66C150+355/4 * 4, SetTo, 1<<(355%4 * 8), 0xFF << (355%4 * 8)),  // ImageID 355 (Zerg Beacon Overlay) SetTo 1: clickable

        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 下方有Zerg Beacon Overlay, 且点击Zerg Beacon Overlay能选中单位

        SetMemory(0x66EC48+4*1, SetTo, 275),  //  ImageID 1 (Scourge shadow): IscriptID 211->275 (Shadow Header) 还原

        CreateUnit(1, "Zerg Scourge", 'Anywhere', CurrentPlayer),  // 正常 Zerg Scourge

    );

}

如何弄出图像为White Circle的单位:

把目标单位的Sprite的imageID改成503 White Circle, 然后把white circle的image设成clickable, Iscript改成 Walking仅有1帧的 Iscript,比如各种空军的Iscript(注意把影子的Iscript设为250),或者金甲子弹的Iscript, 或者260 (ScienceVessel)

0x669E28 Draw function:

0: Normal draw

9: Use Remapping (没用)

10: Shadow

13: 纯绿色

15: Show Rect Size 绿色矩形

16: Hallucination


RemoveUnitAt和KillUnitAt的重大bug:

"loc"为任意一个location,对任意正整数x,有:

KillUnitAt(x, "Terran SCV", "loc", P1);

如果这条Action生效并杀到了任意一个在"loc"内已装机的SCV,则"loc"内所有的P1的已装机的SCV都会被杀掉/清除。

其他可装机的单位同理,其他玩家同理,RemoveUnitAt同理。

造兵时,自己取消造的兵,退全款;出兵建筑被打掉,也退全款。

升级科技时,自己取消升级,退全款;升级建筑被打掉,退75%钱。

关于Z基地菌毯蔓延问题:

编辑器内使用地形或者建筑围住基地,可以使菌毯不蔓延。但是,如果在基地的固有菌毯范围内放置虫族建筑,则会导致菌毯无视地形、无视建筑蔓延开来。

奇怪的现象:

只要触发列表里面有一个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

(在0.9.5.6及之后版本,euddraft默认使用utf8来解码scmd中设置的字符串,因此不再需要加上面这一行。但是,若希望强行使用韩文cp949来解码,则需要手动设置:decodeUnitName : cp949)

关于字符串的编码:

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版本之后,不再需要写py_str,也不再需要手动写\u2009:

settbl("Terran Marine", 0, "简体中文", encoding="utf-8");

因此,如果想将stat_txt.tbl的某一项改为简体中文(以将Terran Marine的tbl text改为"简体中文"为例),完整步骤是:

如果使用的是EE3,则可以直接打开Data Editor - Text,选择 Terran Marine,直接输入"简体中文",保存并编译即可。EE3在编码tbl时,会优先使用cp949编码,若无法被cp949编码,则会使用utf8编码,并自动在字符串末尾添加\u2009字符,详见EE3在编译工程后自动创建的temp\custom_txt.tbl

注: 此为EE3于2021-02-14更新的0.12.3.5版本的新特性,详见:

https://github.com/Buizz/EUD-Editor-3/issues/36

如果你使用的是EE2:

(1) 计算出"简体中文"四个字使用utf8编码后占用的字节数,算出来是12字节。因此真正在tbl中写入的字节为:这12个字节,再加上\u2009\x00这4个字节,总共16字节

python代码:

tbl_str = "简体中文"

bts = tbl_str.encode('utf-8')

n = len(bts)

print(bts)

print(n)

print("<0>" * (n+4))

(2) 在File Manager中打开tbl列表,将"Terran Marine"的text用至少16个<00>来填充,即:

<0><0><0><0><0><0><0><0><0><0><0><0><0><0><0><0>

(3) 写代码 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。

所以,在纯eps工程中,当借用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 (conditionlist中总共16个conditions)的逻辑为:

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为变量(EUDVariable)时,尽量使用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, buildQueue1=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)方法1: build check法

需要选择一个单位作为按钮的载体,这个单位最好选择men单位,而非建筑,具体原因之后讲。此例中以Terran Marine为按钮载体

EE3 - data edit - Button set - Terran Marine - 激活 button list - 右键 - New code - Train Unit - 随便选择一个value作为被造单位(最好选择英雄Unit或者地图中不会用到的unit,假设选择的unit是Jim Raynor枪兵),然后随便选一个图标和注释, Button Work的Condition可以改成3(Always),若需要控制按钮的变亮、变灰,则根据需要来自行定义condition。

然后data edit - Unit - Jim Raynor要修改两个:

default: Mineral和Vespene都改成0

Requirements: Always Use,或者Current Unit is "Terran Marine" (按钮载体单位)

使用变量储存按钮载体单位的ptr/epd地址,然后就可以使用buildcheck和buildreset了

建议使用二模蛤写的BuildCheckConst和BuildResetEPD

示例代码:

if (BuildCheckConst(uepd, 20)) {  // 检测到按钮载体正在建造Jim Raynor (Marine)

    // 执行你想要的功能

    BuildResetEPD(uepd);

}

注意,玩家按下按钮与星际接受到按钮被按下的指令之间有一定的延迟,所以写代码时一定要注意,BuildCheckConst(uepd, 20) 是接受到按钮被按下的指令才会变为True, 而不是玩家按下按钮的时刻

(2)方法2: order法

同样也需要一个单位作为按钮载体,也需要一个“按下即生效”的order作为辅助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... "按钮载体单位"

第二种目的: 按下按钮并选定目标之后触发某功能

Tech-order法:

需要一个order作为辅助order,这个order可以选择没有任何限制的tech(既可作用于空地,也可作用于单位),比如黄雾(Dark Swarm, orderID=119),海盗分裂网(Disruption Web, orderID=181),皇后绿雾(Ensnare, orderID=146)等等。当然,诸如Rally Point (39, 40), patrol (152) 这些可以作用在空地及单位上的order,也可考虑使用,只不过可能要改order的requirement。

下面以SCV作为按钮载体,以order119作为辅助order:

EE3 - data edit - Techdata - [014] Dark Swarm - Usage requirement: 改为 Current Unit is... SCV,或者在is not burrowed之前加两个opcode: "or" "Current Unit is... SCV"

Techdata - [014] Dark Swarm - Default: 消耗能量改为0

(由于order 119 的requirement本来就是alway use, 所以不需要再更改order requirement)

EE3 - data edit - Buttonset - [007] Terran SCV - 把攻击、修理、采矿等无关按钮都删除,然后把蝎子的黄雾按钮给复制粘贴进去

EE3 - data edit - images - [247] SCV - IscriptID 设为含有CastSpell脚本的单位, 比如369 (Corsair)或者 89(Science Vessel Turret)等

注意,为了保证不让载体单位真的把技能释放出来,我们要在代码中检测order时把载体单位的order设为3,这个就需要Iscript的 CastSpell动画中的 sigorder 2 opcode 之前至少有1个wait opcode,以保证单位不会瞬发技能。比如红球的iscript 925和926 就都是瞬发技能,在这里不能使用。

Iscript 369的缺点是释放技能时有分裂网的音效。比较推荐Iscript 89。

代码示例:

const u = EPDCUnitMap(uepd);

const orderTarget_ptr, orderTarget_epd = u.orderTarget;

const tgtX, tgtY = u.getpos("orderTargetPosition");

if (u.orderID == 119) {

    u.orderID = 3;  // 还原orderID,保证每次按按钮仅触发一次

    MoveUnit(1, "Terran SCV", P1, "loc1", "loc1");  // 如果目标离scv很远,则即使把orderID设为3了scv还是会自动走过去,所以需要手动让它停止。"loc1"是一直黏在scv上的location

    const tgt_ptr, tgt_epd = cunitepdread_epd(orderTarget_ptr);

    if (tgt_ptr > 0) {  // 检测到以某单位为order目标

        // 实现相应功能

    }

    else {  // 检测到以空地目标

        // 实现相应功能

    }

}

在使用按钮触发的时候应注意,如果按钮是在建筑上,那么点按钮之后这个建筑或许就真的会造兵(新开辟内存),如果这个按钮在一些men上,那么点按钮之后就不会开辟新内存。

EE2自带的BuildReset function不够严谨

CUnit里面有一个currentBuildUnit (+0xEC)

让任何一个工厂或者你按钮触发的母单位去造一个unit的时候,这个正在被建造的单位都会占用1700个单位中的一个单位,其地址储存在母单位的CUnit的currentBuildUnit中

实际游戏过程中,当我们按ESC取消建造的时候,星际其实做了两件事:

(1) 把母单位(工厂)的buildQueue[buildQueueSlot]设为228

(2) 给currentBuildUnit所指向的单位一个death order,杀掉它(或者直接free这个336字节的内存)

而EE的BuildReset只做了第一件事儿,而没有做第二件事

如果不做第二件事,就会导致Command AtMost可以检测到这个正在被建造的单位。

深入探究按钮造兵的本质:

当按钮的Action为Train Unit (Create Unit)时,按按钮成功之后会发出指令: CMDACT_Train

若干游戏帧后,星际接受到这个指令,即 train_cmd_receive,并调用函数 CMDRECV_Train(u16 wUnitType):

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/recv_commands/train_cmd_receive.cpp

此函数会call HasMoneyCanMake(builder, wUnitType),这个函数 add unit to build queue if successful

然后会setSecondaryOrder(OrderId::Train)

另外,secondaryOrdersRoot(CUnit* unit) 会不断检测unit的secondOrder:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/0_orders/orders_root.cpp

当second order为OrderId::Train时,call function_00468420(CUnit* unit)

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/unit_making/unit_train.cpp

此函数中, (可能是当当前单位为建筑物时?)会执行attemptTrainHatchUnit (在内存中新建CUnit),并会call IscriptAnimation::IsWorking

所以如果想用让建筑来造兵,必须要保证该建筑有IsWorking动画

--------

如何让任意兵造建筑:

以“让Marine 造防空塔”为例:

(1) Buttonset - Terran Marine - 把239号buttonset的防空塔button给复制粘贴过去

(2) Orders - [30]Build - Requiments - 改成Always或者在scv的基础上再加上 Or Current Unit is "Terran Marine" (注意capacity!)

(3) Units - [124]Terran Missile Turret - Requiments: 加上 Or Current Unit is "Terran Marine"

原理:

按钮的Condition是 4(can create Unit/Buiding) 124(Terran Missile Turret)

因此第3步保证了按钮是按钮是亮的?且能按下按钮?

在按下按钮并放下建筑后,会发送一个command,然后:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/recv_commands/CMDRECV_Build.cpp

收到button的command之后,会执行 CMDRECV_PlaceBuildingAllowed 函数检查是否能建造:

第3步保证了 canMakeUnit(builtUnitId, *ACTIVE_NATION_ID) 返回true

第2步保证了 OrderAllowed(builder, orderId, *ACTIVE_NATION_ID) 返回true

注意,当同时选中两个兵时,右下角的按钮面板会变为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

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的CUnit->status (status flag) 有InAir flag,则称单位A为空中单位

如果单位A的CUnit->status (status flag) 没有InAir flag,则称单位A为空中单位

注:若使用触发将units.dat里的单位A的Advanced Flags里面勾上Flying Unit,则再此之后Create的新的A都是空中单位,但地图中现有的单位A皆不受影响。

同理,若使用触发将units.dat里的单位A的Advanced Flags里面uncheck Flying Unit,则再此之后Create的新的A都是地面。

由此可得,一个单位只能是空中单位或者地面单位中的一种。且在地图中同时存在的若干个同一种单位,可能有的是空中单位,有的是地面单位。

一个有武器的单位A,是否会去尝试攻击单位B,取决于单位A所装备的武器(空或地,或都有)是否与目标单位B的空地属性所匹配,即:

若单位A有对空武器(即它的air weapon不是130),且单位B是空中单位,则单位A会尝试攻击单位B。注:此处用“尝试”,意思是之后还要走其他逻辑流程。

若单位A没有对空武器(即它的air weapon是130),且单位B是空中单位,则单位A不会攻击B。

同理,如果单位A有 Ground weapon,且单位B是地面单位,则A会尝试攻击B

如果单位A没有 Ground weapon,且单位B是地面单位,则A不会攻击B

每个单位都可以设置对空武器和对地武器,每个武器(130种武器)也都可以作为每个单位的对空武器或者对地武器。

武器打在目标单位上是否会构成伤害,取决于这个武器的weapon Effect (或称为 explosion type) 以及 target flag是否和目标单位吻合,这里比较复杂

当武器的weapon Effect为air splash时:

如果打出的武器勾上了air而没勾ground,且目标单位是地面单位,则武器不会对目标单位构成伤害。比如100号武器Neutron Flare,它的target flag里面就只有Air而没有Ground,所以如果把100号武器Neutron Flare作为某个单位A的对地武器,则当目标单位B为地面单位时,单位A可以对单位B发动攻击,但是对单位B无法构成伤害。

当武器的weapon Effect为default时:

武器的target flag不管勾不勾ground或air,都会打出伤害(待求证)

上面说的“尝试攻击”,是指单位A的Iscript的对应的function会被调用。

如果单位A有对空武器,尝试攻击空中单位B,则此时单位A的会被给到一个Attack order (10),这个order会调用该单位的image的Iscript的AirAttkInit(或者AirAttkRpt) Animation函数

如果单位A有对地武器,则若地面单位B在A的seek range之内、武器射程之内时,A则会尝试攻击B。此时单位A会被给到AIorder的AttackUnit order,默认情况下为order  10,这个order会调用该单位的image的Iscript的GndAttkInit(或者GndAttkRpt)

orders.dat里面有Animation这一项,含义为Iscript的animation函数,如果值为28,则说明该order所调用的 animation function 是 hardcode的,不随这一项设置的值变化。比如order 10就是 hardcode的,永远调用GndAttkInit。

具体只有以下16种order可以通过更改orders.dat里面的Animation项来改变该order所调用的iscript animation function:

FireYamatoGun1, MagnaPulse (lockdown), DarkSwarm, CastParasite, SummonBroodlings, EmpShockwave, PsiStorm, Irradiate, Plague, Consume, Ensnare, StasisField, Restoration, CastDisruptionWeb, CastOpticalFlare, CastMaelstrom

https://github.com/BoomerangAide/GPTP/blob/master/spells/cast_order.cpp

以枪兵攻击空中单位为例,枪兵尝试攻击空中单位时,AirAttkInit会被调用,而枪兵的AirAttkInit函数是MarineGndAttkInit,即:

MarineGndAttkInit:

playfram        0x00 # frame set 0

wait            1

playfram        0x11 # frame set 1

wait            1

playfram        0x22 # frame set 2

MarineGndAttkRpt:

wait            1

nobrkcodestart

playsnd        69 # Bullet\TMaFir00.wav

attackwith      1

playfram        0x33 # frame set 3

wait            1

playfram        0x22 # frame set 2

wait            1

playfram        0x33 # frame set 3

wait            1

playfram        0x22 # frame set 2

wait            1

playfram        0x33 # frame set 3

wait            1

playfram        0x22 # frame set 2

wait            1

nobrkcodeend   

gotorepeatattk

ignorerest     

MarineGndAttkToIdle:

playfram        0x11 # frame set 1

wait            1

playfram        0x00 # frame set 0

wait            1

goto            MarineWalkingToIdle

以上脚本执行到attackwith 1时,才会真正创建一个子弹。

注:attackwith 1的含义是“使用该单位的 ground weapon进行攻击”,如果数字是除了1以外的数,则代表用air weapon进行攻击。

所以说,无论你把枪兵的air weapon改成什么,只要枪兵有air weapon,那么它都会使用ground weapon来攻击

创建一个子弹flingy,此时238号Iscript的Init被执行,执行到domissiledmg命令时,才会真正对被攻击的目标造成伤害。

具体逻辑详见星际代码:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp#L96-L135

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/base_orders/attack_orders.cpp

举例:

当A为Terran Marine、B为Terran Medic、w为Gauss Rifle时:

A仅有对地武器w(将对空武器设为130),单位B为地面单位(未勾Flying Unit),无论w勾Air/勾Ground/都勾/都不勾,都是: A可攻击B,并有伤害

A仅有对地武器w,单位B为空中单位(勾了Flying Unit),无论w勾Air/勾Ground/都勾/都不勾,都是: A不能攻击B

A仅有对空武器w(将对地武器设为130),单位B为地面单位,无论w勾Air/勾Ground/都勾/都不勾,都是: A不能攻击B

A仅有对空武器w,单位B为空中单位,无论w勾Air/勾Ground/都勾/都不勾,都是: A可攻击B,但是没有伤害,且会造成overflow,导致出现不可预测的现象(会随着星际的小版本的改变或星际32bit/64bit而改变),比如出现石头,或者游戏崩溃,

第4种情况的详细解释:

枪兵攻击护士时,枪兵的Iscript的AirAttkRpt或GndAttkRpt)函数会执行,当然,具体call谁并不重要,因为两个都指向MarineGndAttkRpt函数。

MarineGndAttkRpt函数中有"attackwith 1"脚本,这个脚本会让枪兵的 Group Weapon生效("attackwith 2"就是让Air Weapon生效)。由于Ground Weapon是130,所以星际会读取 Weapon.dat里面每一项的+4*130 offset,这个是典型的读取溢出,会读到不可预测的内容,也就会读取到不可预测的Graphic,因此可能会出现石头,也可能导致其他不可预测的事情。

关于星际1处理单位攻击的底层逻辑(各种武器、子弹、伤害的底层逻辑):

实际情况较为复杂,需要具体问题具体分析。

例1: 雷车攻击

当雷车选定了攻击目标(某地面单位)并准备攻击时,其orderID为10,此时它的 iscript(id=86) - AttkGround 被call:

VultureGndAttkInit:

    wait            1

    attackwith      1

    gotorepeatattk

    goto            VultureGndAttkToIdle

当 attackwith 1 被执行时(1代表对地武器),函数 CUnit::fireWeapon(u8 weaponId) 被call,即位于0x00479C90的 fireWeaponHook(CUnit* unit, u8 weaponId) 被call,其中 weaponId为雷车的对地武器:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_fire.cpp

fireWeapon这个函数一般会被attackwith, attack, castspell这几个opcode call

这个函数会执行: createBullet(unit, weaponId, x, y, unit->playerId, unit->currentDirection1)

此时会生成一个CBullet. CBullet是一个类似于CUnit的struct,详见:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/CBullet.h

createBullet函数的具体内容不明。但可以推测,这个函数会获取weaponID的信息,比如behavior/effect等,并决定是否要寻找unit->orderTarget,即attacker的目标CUnit的地址。还有这个bullet是否会miss (CBullet::hitFlags),等等,并将这些信息写入被Create出来的CBullet中。(所以说,一个武器是否会miss,是在它生成的时候就定下来的,而不是它打到目标上时)

之后,这个CBullet的image的iscript的Init被call,以雷车的 Fragmentation Grenade 为例:

它的init被call时所执行的opcode并不会造成任何伤害。成功create之后,该Fragmentation Grenade会根据自己的CBullet信息来飞向目标单位,并在到达指定地点时call iscript的 death anim (整个过程应该是由某个未知函数所控制的):

imgol          440 0 0 # Fragmentationgrenadehit (thingy\efgHit.grp)

domissiledmg   

wait            1

end           

其中domissiledmg才是真正对目标单位造成伤害的opcode。 它的底层逻辑是:

opcode domissiledmg 会call这个函数: weaponImpact(CBullet* bullet):

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_impact.cpp

这个函数会根据weapon的effect来决定执行的效果,以 Fragmentation Grenade为例,其effect为WeaponEffect::NormalHit,因此进行简单逻辑检查后,若没miss的话就call这个函数:

WeaponBulletHit_Helper(bullet, target, damage_divisor), 即位于0x00479AE0的 WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor):

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/wpnspellhit.cpp

target->damageWith(damage, weaponId, attacker, attacker->playerId, attacker->currentDirection1, 1):

即 CUnit::damageWith(s32 damage, u8 weaponId, CUnit* attacker, u8 attackingPlayer, s8 direction, u8 damageDivisor):

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/CUnit.cpp

次函数又会call Func_DoWeaponDamage, 即:

weaponDamageHook(s32 damage, CUnit* target, u8 weaponId, CUnit* attacker, u8 attackingPlayerId, s8 direction, u8 dmgDivisor)

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/weapons/weapon_damage.cpp

这个函数才是真正对目标造成伤害的函数,执行我们所熟悉的伤害值计算的逻辑。

计算出伤害结果之后,还会call CUnit::damageHp函数,此函数又会call killTargetUnitCheck 来检查被攻击的target单位是否死亡

本例为 iscript中opcode为 attackwith时的一个特定例子。

简单来讲,iscript的attackwith会生成一个CBullet,此CBullet在death时会执行iscript的domissiledmg,并对目标造成伤害。

小知识: domissiledmg只能作用于CBullet,若作用于CUnit则游戏崩溃

例2: 架起来的坦克的攻击

与雷车相似,但是子弹有区别,坦克的子弹是直接生成的target上,并且init anim会直接执行到death,并执行domissiledmg。

之后在 weaponImpact(CBullet* bullet)中,由于arclite cannon shot的effect是 splash around target (WeaponEffect::SplashRadial),

故会执行到 FindBestUnit(&area_of_effect, Func_SplashProc, bullet)

这个函数会对范围内的每个单位都call WeaponBulletHit(CBullet* bullet, CUnit* target, u32 damageDivisor),并计算伤害。

FIndBestUnit函数的具体内容不明,疑似会调用 bool canWeaponTargetUnit(u8 weaponId, CUnit* target, CUnit* attacker) 函数:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/api.cpp

如何让科学球称为可以对空对地攻击的单位:

给科学球加对地、对空武器

改Acquisition range

AI-order RC: 1

Iscript设成frame不多的、有对空对地攻击的、有castspell的,比如66号(battle cruiser)

若嫌shadow烦,则可把shadow的draw function变成隐形,或者iscript变成直接death的

单位在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

星际的重要特性:

本地指令(game command)从“发出”到“共享完成”之间存在延迟,单机游戏这个延迟大约为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,注:当x为常数而非EUDVariable时,应该用BuildCheckConst函数

    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不会生效。所以这样才能保证,在按钮处于“亮”状态时,无论玩家以多快的速度连续按多少次按钮,都只能触发1次 Create枪兵

游戏内看到的图层跟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++;  // 错误! 从euddraft0970开始,此写法正确

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-based index):

0: 0x59CCA8

1: 0x59CCA8 + 336 * 1699 (Unit Node Table的最后336byte)

2: 0x59CCA8 + 336 * 1698 (Unit Node Table的倒数第二个336byte)

...

1699: 0x59CCA8 + 336 * 1

alphaID (一个u16数据)的定义:

Unit Alpha ID = (uniquenessIdentifier << 11) | index = (uniquenessIdentifier << 11) + index

uniquenessIdentifier: 某个CUnit内存被CUnit占用的次数, 即CUnit +0xA5所储存的u8数据

index: 即1-based index: (unitPTR - 0x59CCA8)/336 + 1

https://github.com/armoha/euddraft/issues/93

alphaID的低11位是CUnit的1-based index,高5位是uniquenessIdentifier的低5位

当uniquenessIdentifier=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, 即CUnit的1-based index

即 MemoryIndex = AlphaID & 0b0111 1111 1111 即AlphaID的低11bit

即 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

注:单位进出运输机,会导致该单位在链表上的相对位置变动,貌似会被移到表头,但该单位所占用的内存地址不变。

Armo — 2022/09/23

Unload Transport/Bunker/Interceptor, Workers Exit Gas Buildings etc re-link unit on top of linked list

Both newly created unit and unloaded unit link to first unit pointer so for EUDLoopNewUnit I had to differentiate them using CUnit+0xA5 uniquenessIdentifier

综上所述:

假设地图中总共有5个预置单位(无任何unit sprite),其unitIndex分别为0, 1, 2, 3, 4(SCMD使用unitIndex这个概念来表示单位被摆下的先后顺序),假设这5个单位均只占用1个Node(不存在SubUnit),则当它们都创建完毕时:

链表:unit1 -> unit5 -> unit4 -> unit3 -> unit2

内存:  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

另外注意,星际优先加载预置的unit sprite,然后再加载预置的units

主机名后缀#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这个内存的值。

如何禁止玩家使用#xN后缀(#x suffix)

执行以下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

使得核弹造成伤害的脚本是:

image 316 (NuclearMissile) 的iscript (131) 的SpecialState1.

详见GPTP:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/spells/nuke_orders.cpp

current_image->playIscriptAnim(IscriptAnimation::SpecialState1);

核弹/原子弹 伤害计算:

(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: 单位的总防御,等于单位的盾防+目前所拥有的盾值+血防

s: 根据武器类型与被攻击单位体型所得的 multiplier

运算符号:

- : 减法运算符

* : 乘法运算符

/ : 整数除法运算符,结果是整数除法得到的商。例: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状态并不能让该雷车提速。

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为0x0C,即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)

紧接着iscript 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(0x16)或者GOTO(0x07)这两个opcode

因此,这些OPCode翻译过来是:

imgul          339 0 0 # RagnasaurShad (neutral\nacShad.grp)

playfram        0x66 # frame set 6 (本质是frame index 0x66)

goto            RagnasaurLocal00

其中:

OPcode 09即imgul, 53 01 00 00 即0x153, 0, 0, 即在(0,0)处的下方图层创建339号image

OPcode 00: playfram, 66 00: 0x66

OPcode 07: goto, BA 79: offset+0x79BA

其中 0x79BA的字节是:

05 7D 07 BA 79

wait 125

goto +0x79BA

无限循环

注意,WalkingToIdle在 +0x1D25,也就是从Init的第6字节开始的

所以我们可以看到,Init相当于先执行了09 53 01 00 00这5个字节之后,开始进入WalkingToIdle的内容

再看Death,从+0x1D2B开始的字节是:

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

playsnd        53 # Misc\CRITTERS\LCrDth00.wav

setfldirect    0

playfram        0x99 # frame set 9

wait            1

playfram        0x9a # frame set 9

wait            1

playfram        0x9b # frame set 9

wait            1

playfram        0x9c # frame set 9

wait            1

playfram        0x9d # frame set 9

wait            1

playfram        0x9e # frame set 9

wait            1

playfram        0x9f # frame set 9

wait            1

playfram        0xa0 # frame set 9

wait            1

end   

注:

[18]: playsnd, [35 00]: 53

[34]: setfldirect, [00]: 0

[00]: playfram, [99 00]: 0x99

[05]: wait, [01]: 1

[00]: playfram, [9A 00]: 0x9A

...

[16]: end

OPcode及其含义的对照表见:

iscript_codes.txt

注意,当遇到无意义的OPcode(比如0xE6, 0x97, 0x54, 0x45等等),星际都会直接无视(即nop),然后接着读下一个字节。

所以,如果在魔改时,某个iscript function被call了之后发现它所在的地址位于iscript.bin的前1368字节中的某一个字节,就会导致一直[00 00 00]即playfram 0,其他都是无效字节,然后读到 +0x560 (即+1368)处,读到了:

0F 00 00 00 84 05 20 79 20 79 20 79 00 00 ...

[0F]: sprol <SpriteID> <SByte x> <SByte y>

所以它所做的事情就是 spawn a sprite with ID 0x0000 at offset position (0, 0x84) 即(0, -125) relative to the current overlay.

而第0号sprite是石头。这也就是为何在读到缺失的iscript脚本时,会在单位的正上方125像素处出现石头:

比如上面198号iscript因某些原因被调用了GndAttkInit函数,而GndAttkInit函数所对应的索引信息是00 00,所以sc就会跳到iscript.bin的最开头去读取opcode,也就出现了刚刚所讲的情况。

重点讲一下 playfram这个opcode

假设目前的脚本时 playfram N, 则:

当该image的 GraphicTurns=0 (未开启状态)时,playfram N读取的就是其GRP的第N帧(从0开始记)

当该image的 GraphicTurns=1 (开启状态)时,playfram N读取的就是其GRP的第N+offset帧, offset取值范围是0-16

其中 offset由当前CUnit/CBullet的 direction (CUnit+0x21)决定,以下是对应表:

direction, frameIndex(offset)

0  -  3:  0

4  - 11:  1

12 - 19:  2

...

116-123: 15

124-131: 16

132-139: 15 (mirrored)

140-147: 14 (mirrored)

...

244-251:  1 (mirrored)

252-255:  0 (mirrored)

举例:

当人族SCV执行了Iscript指令 playfram 0x11 的时候:

假设它此时的direction是17, 则查表可得offset为2, 所以此时播放的是scv.grp的 0x11+2 = 0x13,即编号为19的帧

假设它此时的direction是140, 则查表可得offset为14 (mirror), 所以此时播放的是scv.grp的 0x11+14 = 0x1F,即编号为31的帧的镜像图片

所以说,在星际的默认设置下,凡是有GraphicTurns的image,其对应GRP所包含的图片数都是17的倍数,且以17帧为一组

貌似只有elevation level >= 12的men才能通过修改CUnit+0x21(CurrentDirection)来改变其图片,具体还需更多试验

如何检测某个人类玩家退出游戏/Defeated:

在active player structure里面:

bread(0x57EEE0 + 36 * p + 0x8) == 10

MemoryXEPD(EPD(0x57EEE0) + 2 + 9*p, Exactly, 10, 0xFF)

若是电脑玩家被defeat,则是11

单位攻击的优先级

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_priority.cpp

详见getAttackPriorityHook(CUnit* target, CUnit* attacker)

cammove:

https://cafe.naver.com/edac/78006

cammove的功能:

让你的屏幕(视角)一直跟随某一个特定的location,类似于不断CenterView,但是视角不会瞬间切过去,而是会以一定的加速度、最大速度逐渐移动过去。

cammove使用方法

注: 带有尖括号"<>"的内容是你的自定义内容

步骤:

(1) 在地图中创建一个名为"cammoveLoc"的location, 并且不能在任何触发中使用这个location。此location是cammove插件的辅助location,它将被插件以location名称识别,因此只能命名为"cammoveLoc",但其locationID无关紧要。

(2) 将任意一个switch命名为"cammove"。同理,插件会以名称识别该switch,故它的switchID不重要。此switch为cammove的开关,当此switch的状态为Set时,cammove插件便会自动执行如下功能:

让你的屏幕一直跟随某个特定的location(即下一步中你所设定的location)。当此开关的状态为Cleared时,cammove插件不生效。

(3) 在edd文件中写:

[camMove]

targetloc: <你想要让屏幕跟随的location>

inertia: <某个整数>

maxspeed: <某个整数>

其中,inertia为屏幕移动的惯性(加速度?),maxspeed为屏幕移动的最大速度

(4) 必须加[eudTurbo]!

(5) 多人游戏模式下才可生效

动了任何跟按钮相关的内存(0x5187E8的s_button Offset及其之前的button info)之后,会导致在游戏中同时选中机枪+火兵(或英雄机枪火兵)时不显示兴奋剂按钮。

解决办法是在新单位出现时手动修改其CUnit的 currentButtonSet, 使得它们的buttonset统一为某个单位的buttonset:

function beforeTriggerExec() {

    foreach(ptr, epd: EUDLoopNewUnit()) {

        const u = EPDCUnitMap(epd);

        if (u.unitType == $U("Terran Firebat") ||

            u.unitType == $U("Jim Raynor (Marine)") ||

            u.unitType == $U("Gui Montag")

        ) {

            u.currentButtonSet = $U("Terran Marine");

        }

    }

}

https://cafe.naver.com/edac/85055

Armo - 2022-09-27 16:40

To put it more accurately, unchecking 'Do not become guard' in EE2 AI tab will crash SC when it dies, owned by Computer, and its type is overlord, larva or workers.

神族农民放下建筑时的音效:

protoss\shuttle\pshbld00.wav

神族建筑造好时的音效:

protoss\shuttle\pshbld03.wav

塔防图必备技巧,让普通men单位模拟防御塔,即单位无法移动,但可以通过鼠标右键来自选攻击目标:

(1) Units (以Terran Vulture为例) - AI Order - Unit Attacks: 改成 19 (Tower), RClick Order: 改成 3

(2) Orders - [019] - Requirements: Always Use

(3)* 若塔可能被攻击,则需要uncheck掉advanced - Special Ability Flags的 Full Auto-attack

但是以上只适用于普通men,不适用于金甲和航母。金甲和航母需要特殊处理

当无法手动选择目标时,SC根据如下代码来操纵单位的attack priority攻击优先级:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_priority.cpp

让Ion Cannon可以像普通防御塔一样攻击:

(1) 对空对地武器设为 30(Yamato Gun)

(2) Seek range 设为非0。注意,如果武器射程比seek range短,则单位可能会在自动获取目标之后发呆不攻击(因为射程不够)

(3) AI order设为跟photon cannon一样

(4) 30号weapon的attack angle改成128

(5) Ion Cannon的Iscript改成120 (Missile Turret (Turret))

(6) 若想实现右键手动选择目标,则需要设置order 19的 requirement: Always Use 或者 加一个Ion Cannon

(7) Button Set 改成162 (Photon cannon的buttonset)

注意,Iscript120的death动画是直接end,所以并不会有爆炸动画,效果像是RemoveUnit

让小狗可以攻击空中单位(本质是如何让iscript的attackmelee指令可以攻击空中单位):

(1) 给小狗添加任意对空武器: 目的是让小狗可以尝试攻击空中单位

(2) 小狗的AI order的attackUnit改成任意一个animation为castspell魔法order,比如115 (鬼兵锁定技能)

(3) 将 order115的 select target 改成一个可对空和对地的武器,比如35号武器(小狗自己的武器),然后把武器的对空flag给勾上

(4) 将 order115的 energy改成 44(None): 即不需要消耗魔法。等价地,也可以把115所对应的技能(鬼兵锁定技能)的魔法消耗设为0

(5) 将 order115的 animation 改成 2(Ground attack default),目的是让小狗获得order115之后,call iscript的GndAttkInit function而不是castspell、也不是AirAttk

(6)* 注意该order是否有requirement,若有,则要加上小狗,或者索性设为always

(7)* 做完以上所有之后,小狗可以自动攻击空中单位1次,但是无法自动连续攻击,需要手动操作攻击(因为order属于cast spell,使得单位无法连续攻击)。若想让小狗连续攻击空中单位,需要用触发手动更改小狗的CUnit order等

(8)* 注意将小狗的 seek range 改成大于武器射程的值

以上这样做的后果是,右键敌人后小狗并不攻击,必须主动A,且射程、cooldown、对空对地flag等等是由order115的 select target所选的武器而决定的,但伤害却是小狗对地武器的伤害。并且当小狗站在敌人空中单位正下方并H住时,小狗会调用普通对空攻击,并调用AirAttk,出现石头

让航母小飞机interceptor不出仓,顶在航母母体上攻击:

把intercepter flingy(41号)的movement control改成2 (iscript.bin)

Top vs Bottom (TvB 上对下)模式,固定每个玩家的颜色:

在scmd中,让任何一个玩家使用自定义颜色,即可。

虫族基地与虫卵 hatchery and larva:

虫族基地CUnit里面并没有任何指针用来记录其所连接larva,但是larva里面有指针记录它所连接的基地。

虫族基地决定是否生产新的larva,是由larvaCounter结果是否<3决定的。larvaCounter函数会在基地周围的区域loop所有的larva,如果larva的connectUnit是本基地,则larvaCount+1,详见 secondaryOrd_SpawningLarva:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/orders/larva_creep_spawn.cpp

选中虫族基地后按S,星际所做的事情也是loop基地周围的区域,找到所有connectedUnit是本基地的larva,然后选中它们。

如果使用MoveUnit将larva移走,则这些larva的connectUnit仍是原来的基地,不会变,并会因为order的原因自动往它原来的基地爬。

如果使用MoveUnit将基地走,则它本来的larva都会失去连接,开始乱跑。对于失去连接的larva,可以用如下触发来使其不乱跑:

RunAIScriptAt("Set Generic Command Target", "loc1");

RunAIScriptAt("Make These Units Patrol", "loc1");

具体功能存疑,待试验

units.dat 的 movement flags 0x660FC8的每个bit的含义:

0x01: can move?

0x02:

0x04: flyer?

0x08:

0x10:

0x20:

0x40: can move?

0x80: hovering?

根据units.dat推测:

所有不能动的,都是1个flag都没有

所有只能在地面动的,都有0x01, 0x40

所有地面悬浮的,都有0x01, 0x40, 0x80

所有能飞的(包括人族建筑), 都有上面提到的全部4个flags

注意,CUnit中也有movementFlags,也是1字节,但与units.dat中的movement flags (+0x20)含义不同,见:

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/SCBW/structures/Cunitlayout.h

关于attack cooldown:

Bool32 attackApplyCooldown(CUnit* unit)

https://github.com/BoomerangAide/GPTP/blob/master/GPTP/hooks/attack_and_cooldown.cpp

此函数被inject到 0x00478B40

GPTP中,别的函数call它时call的是 attackApplyCooldown_Helper

注意,这个函数是在单位处于持续攻击状态之下被call的,因此当cooldown为0时,call的是AttkRpt函数而不是AttkInit。

而如果单位是从Idle状态转变为攻击状态,(尚未在GPTP中找到对应的函数)则命令单位攻击时,单位会先收到order 10,然后站着发呆,直到它的cooldown变成0,然后瞬间call AttkInit,同时将cooldown设为cooldown+rand({-1, 0, 1, 2})

但是枪兵等单位的AttkInit需要执行若干帧后才会进入执行真正的attack with (或其他attack)opcode,如果在执行到attack opcode之前单位被叫停(按了stop,或者单位被remove/death等),则单位就在未开火的情况下,cooldown还被设为最大值了。所以检测单位的cooldown突变,并不100%意味着单位成功开火

无论单位处于什么状态,它的cunit cooldown都会每帧减小1,直到0

当单位处于持续攻击状态时,由于星际内部代码会在cooldown减小到0时让单位开火,并把cooldown设为其武器cooldown+rand({-1, 0, 1, 2}),然后call AttkRpt

故当单位处于持续攻击状态时,用eps代码检测cunit cooldown时无法检测到0,只能检测到cooldown降到1,然后下一帧就升高到cooldown+rand({-1, 0, 1, 2})

检测某特定单位(CUnit)开火的瞬间:

方法(1): 检测cooldown

每帧都读取该CUnit的cooldown (以对地武器cooldown为例) 储存其上一帧的cooldown (记作cd_last),那么它开火的瞬间满足以下充要条件:

u.gCooldown > 0 && cd_last <= 1

方法(2): 检测cooldown

方法2不如1完美,不能检测开火的瞬间,但却能与每次开火对应: 检测gCooldown==weaponCooldown-1的瞬间

检测某类单位开火的瞬间:

以雷车为,想要检测全图中所有雷车的开火瞬间,可以在weapons.dat中将雷车武器的cooldown +100,然后在eps中loop全部雷车:

if (u.gCooldown >= 100) {  // 检测到雷车开火瞬间

    u.gCooldown -= 100;

    // 做其他事情,比如创建特效等

}

以上的方法本质上都是检测cooldown而已,并不代表单位真的开火了。所以如果想用这种方法检测单位开火,则要保证单位的武器是瞬发,即AttkInit脚本在wait1后直接执行 attack opcode,没有前摇。

方法(3): 检测nonbreakable doe start:

检测 status flag (CUnit+0xDC) 的 0x2000, NoBrkCodeStart (Iscript)

因为很多单位的AttkInit animation都有NoBrkCodeStart, 这是比检测cooldown 更精确的方式。

if (u.orderID==10 && u.check_status_flag(0x2000)) {

    检测到开火瞬间

}

所有能释放魔法(cast spell)的单位总结(很难定义什么叫能释放魔法,下面根据不同定义进行列举)

拥有CastSpell动画且CastSpell里面有castspell opcode的 (castspell opcode之后必有 sigorder 2):

Zerg Defiler (46, 52): ImageID=13, IscriptID=9

Infested Kerrigan: ImageID=33, IscriptID=20

Zerg Queen (45, 49): ImageID=46, IscriptID=27

Terran Ghost (1, 99, 100, 104): ImageID=228, IscriptID=70

Sarah Kerrigan: ImageID=237, IscriptID=77

Science Vessel: ImageID=260, IscriptID=88

Protoss Arbiter (71, 86): ImageID=130, IscriptID=146

Dark Templar (Hero): ImageID=129, IscriptID=152

Protoss High Templar (67, 79, 87): ImageID=126, IscriptID=158

Terran Medic: ImageID=944, IscriptID=360

Protoss Dark Archon: ImageID=925 (Dark Archon Energy), IscriptID=365

*Protoss Dark Archon: ImageID=926 (Dark Archon Being), IscriptID=366

Protoss Corsair (60, 98): ImageID=929, IscriptID=369

Protoss Dark Templar: ImageID=933, IscriptID=372

拥有CastSpell动画但是里面没有castspell opcode的:

IscriptID=66: ImageID=218, unitID: Terran Battle Cruiser (4 kinds: 12, 27, 28, 29, 102)

IscriptID=89: ImageID=261 (Science Vessel Turret)

IscriptID=290 (CastSpell=Death): imageID=120(Shuttle Glow), imageID=114(Carrier Glow)

IscriptID=291 (CastSpell=Death): imageID=132, 220, 225, 249, 941, 942, 943

IscriptID=370 (CastSpell=Death): imageID=931 (Corsair Overlay)

含有castspell opcode但并不是位于CastSpell动画的:

Spider Mine: ImageID=258, IscriptID=87, SpecialState1

Nuclear Missile, ImageID=316, IscriptID=131, SpecialState1

*ImageID=543 (Yamato Gun Overlay), IscriptID=314, Init

0x6283F0: usedUnitCount: CUnit occupancy, number of CUnits taken up