“MSQC”的版本间的差异

来自星际争霸重制版地图研究所
跳到导航 跳到搜索
→‎例7
 
(未显示同一用户的3个中间版本)
第8行: 第8行:
作者于2019年4月1日更新了此插件,添加了一些功能,并将文件名更换为MSQC.py
作者于2019年4月1日更新了此插件,添加了一些功能,并将文件名更换为MSQC.py


由于EUD地图可以随机读写内存,经常会导致作者无意中写出异步触发(或异步代码)导致玩家掉线
由于EUD地图可以随意读写内存,经常会导致作者无意中写出异步触发(或异步代码)导致玩家掉线


针对这种现象,韩国的大神想到了一个方法,可以把非共享内存转化为共享内存,然后再以它为条件,使得我们可以在多人游戏中使用“掉线触发”,而游戏不掉线。他写的这个插件叫做MSQC
针对这种现象,韩国的大神想到了一个方法,可以把非共享内存转化为共享内存,然后再以它为条件,使得我们可以在多人游戏中使用“掉线触发”,而游戏不掉线。他写的这个插件叫做MSQC
第589行: 第589行:


var send = 0; //eps代码中的所有变量均为私有变量(non-shared variable),即其所在地址为non-shared memory
var send = 0; //eps代码中的所有变量均为私有变量(non-shared variable),即其所在地址为non-shared memory
const receive = PVariable(); //写成receive = EUDArray(8)或者receive = [0, 0, 0, 0, 0, 0, 0, 0]也是一样的
const receive = PVariable(); //写成receive = EUDArray(8)或者receive = [0, 0, 0, 0, 0, 0, 0, 0]也是一样的
function onPluginStart() {
function onPluginStart() {
 
    EUDRegisterObjectToNamespace("send", send);
EUDRegisterObjectToNamespace("send", send);
    EUDRegisterObjectToNamespace("receive", receive);
 
EUDRegisterObjectToNamespace("receive", receive);
 
}
}
function afterTriggerExec() {
function afterTriggerExec() {
 
    if(Memory(0x006CDDC4, AtMost, 319) && Memory(0x006CDDC8, AtMost, 239))
if(Memory(0x006CDDC4, AtMost, 319) && Memory(0x006CDDC8, AtMost, 239))
        send = 1; //本地玩家鼠标指针位于屏幕左上区域,将变量send的值设为1
 
    else
send = 1; //本地玩家鼠标指针位于屏幕左上区域,将变量send的值设为1
        send = 0; //鼠标不在左上区域时,变量send值设为0
 
    foreach(p: EUDLoopPlayer("Human")) {
else
        if (receive[p] == 1)
 
            CreateUnit(1, "Terran Marine", "Anywhere", p); //为在t=100帧时鼠标在屏幕左上的玩家创建一个枪兵
send = 0; //鼠标不在左上区域时,变量send值设为0
    }
 
foreach(p: EUDLoopPlayer("Human")) {
 
if (receive[p] == 1)
 
CreateUnit(1, "Terran Marine", "Anywhere", p); //为在t=100帧时鼠标在屏幕左上的玩家创建一个枪兵
 
}
 
}
}


第660行: 第645行:


var send = 0;
var send = 0;
const receive = PVariable();
const receive = PVariable();
function onPluginStart() {
function onPluginStart() {
 
    EUDRegisterObjectToNamespace("send", send);
EUDRegisterObjectToNamespace("send", send);
    EUDRegisterObjectToNamespace("receive", receive);
 
EUDRegisterObjectToNamespace("receive", receive);
 
}
}
function afterTriggerExec() {
function afterTriggerExec() {
 
    send = 1; //每轮扫触发时都先将send的值设为1
send = 1; //每轮扫触发时都先将send的值设为1
    if(Memory(0x006CDDC4, AtLeast, 320))
 
        send += 1; //本地玩家鼠标指针位于屏幕右半边时,将变量send的值设加1
if(Memory(0x006CDDC4, AtLeast, 320))
    if(Memory(0x006CDDC8, AtLeast, 240))
 
        send += 2; //本地玩家鼠标指针位于屏幕下半边时,将变量send的值设加2
send += 1; //本地玩家鼠标指针位于屏幕右半边时,将变量send的值设加1
    foreach(p: EUDLoopPlayer("Human")) {
 
        if (receive[p] == 1) //玩家p+1在t=100帧时鼠标位于屏幕左上区域
if(Memory(0x006CDDC8, AtLeast, 240))
            CreateUnit(1, "Terran Marine", "Anywhere", p);
 
        if (receive[p] == 2)) //玩家p+1在t=100帧时鼠标位于屏幕右上区域
send += 2; //本地玩家鼠标指针位于屏幕下半边时,将变量send的值设加2
            CreateUnit(1, "Terran Ghost", "Anywhere", p);
 
        if (receive[p] == 3)) //玩家p+1在t=100帧时鼠标位于屏幕左下区域
foreach(p: EUDLoopPlayer("Human")) {
            CreateUnit(1, "Terran Vulture", "Anywhere", p);
 
        if (receive[p] == 4)) //玩家p+1在t=100帧时鼠标位于屏幕右下区域
if (receive[p] == 1) //玩家p+1在t=100帧时鼠标位于屏幕左上区域
            CreateUnit(1, "Terran Goliath", "Anywhere", p);
 
        }
CreateUnit(1, "Terran Marine", "Anywhere", p);
 
if (receive[p] == 2)) //玩家p+1在t=100帧时鼠标位于屏幕右上区域
 
CreateUnit(1, "Terran Ghost", "Anywhere", p);
 
if (receive[p] == 3)) //玩家p+1在t=100帧时鼠标位于屏幕左下区域
 
CreateUnit(1, "Terran Vulture", "Anywhere", p);
 
if (receive[p] == 4)) //玩家p+1在t=100帧时鼠标位于屏幕右下区域
 
CreateUnit(1, "Terran Goliath", "Anywhere", p);
 
}
 
}
}


第751行: 第714行:


const mouseXaddr = 0x6CDDC4;
const mouseXaddr = 0x6CDDC4;
const mouseYaddr = 0x6CDDC8;
const mouseYaddr = 0x6CDDC8;
const mouseXY_rc = PVariable(); //received
const mouseXY_rc = PVariable(); //received
const mouseX_rc = PVariable();
const mouseX_rc = PVariable();
const mouseY_rc = PVariable();
const mouseY_rc = PVariable();
var mouseX_local = dwread_epd(EPD(mouseXaddr));
var mouseX_local = dwread_epd(EPD(mouseXaddr));
var mouseY_local = dwread_epd(EPD(mouseXaddr));
var mouseY_local = dwread_epd(EPD(mouseXaddr));
var mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里
var mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里
function onPluginStart() {
function onPluginStart() {
 
    EUDRegisterObjectToNamespace("mouseXY_local", mouseXY_local);
EUDRegisterObjectToNamespace("mouseXY_local", mouseXY_local);
    EUDRegisterObjectToNamespace("mouseXY_rc", mouseXY_rc);
 
EUDRegisterObjectToNamespace("mouseXY_rc", mouseXY_rc);
 
}
}
function afterTriggerExec() {
function afterTriggerExec() {
 
    mouseX_local = dwread_epd(EPD(mouseXaddr));
mouseX_local = dwread_epd(EPD(mouseXaddr));
    mouseY_local = dwread_epd(EPD(mouseYaddr));
 
    mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里,每一轮扫触发都要更新
mouseY_local = dwread_epd(EPD(mouseYaddr));
    foreach (p: EUDLoopPlayer("Human")) {
 
        mouseY_rc[p], mouseX_rc[p] = div(mouseXY_rc[p], 0x10000); //得到的纵坐标和横坐标分别是mouseXY_rc除以0x10000的商和余数
mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里,每一轮扫触发都要更新
        //当玩家p+1的MSQC指令未触发时,mouseXY_rc[p]的值为0xFFFFFFFF,此时拆解到的mouseY_rc和mouseX_rc值均为0x0000FFFF
 
    }
foreach (p: EUDLoopPlayer("Human")) {
    const cp = getcurpl();
 
    setcurpl(getuserplayerid());
mouseY_rc[p], mouseX_rc[p] = div(mouseXY_rc[p], 0x10000); //得到的纵坐标和横坐标分别是mouseXY_rc除以0x10000的商和余数
    foreach(p: EUDLoopPlayer("Human")) {
 
        StringBuffer(256).printfAt(p, "玩家{}的鼠标指针在其屏幕内的坐标为: {}, {}", p+1, mouseX_rc[p], mouseY_rc[p]);
//当玩家p+1的MSQC指令未触发时,mouseXY_rc[p]的值为0xFFFFFFFF,此时拆解到的mouseY_rc和mouseX_rc值均为0x0000FFFF
    }
 
    setcurpl(cp);
}
 
const cp = getcurpl();
 
setcurpl(getuserplayerid());
 
foreach(p: EUDLoopPlayer("Human")) {
 
StringBuffer(256).printfAt(p, "玩家{}的鼠标指针在其屏幕内的坐标为: {}, {}", p+1, mouseX_rc[p], mouseY_rc[p]);
 
}
 
setcurpl(cp);
 
}
}



2023年3月25日 (六) 20:58的最新版本

简介&功能

MSQC,全名为MurakamiShiina QueueCommand,一个功能及其强大的插件

其中MurakamiShiina应该是作者喜欢的一个acg人物,而QueueCommand则是该插件运作的基本原理

旧版MSQC插件的文件名叫MurakamiShiinaQC.py,最初发行于2017年

作者于2019年4月1日更新了此插件,添加了一些功能,并将文件名更换为MSQC.py

由于EUD地图可以随意读写内存,经常会导致作者无意中写出异步触发(或异步代码)导致玩家掉线

针对这种现象,韩国的大神想到了一个方法,可以把非共享内存转化为共享内存,然后再以它为条件,使得我们可以在多人游戏中使用“掉线触发”,而游戏不掉线。他写的这个插件叫做MSQC

MSQC是“MurakamiShiina QueueCommand”的首字母,其中MurakamiShiina应该是作者喜欢的一个acg人物,而QueueCommand则是该插件运作的基本原理

掉线触发(desync trigger)

我们在星际争霸的游戏中时,所有的游戏数据都会储存在每个玩家的内存里

我们知道,触发分为条件和动作两部分(触发执行者本质上其实也属于条件)

本质上来讲,“条件”就是读取某个内存地址中的内容并根据内容作出一个判断,“动作”就是向某个内存地址中写入一些内容

从功能上来讲,内存分为两种:共享内存(shared memory) 和 非共享内存(non-shared memory)

共享内存地址中的储存的数据叫做共享数据(shared data),非共享内存地址中储存的数据叫做非共享数据(non-shared data / local data)

游戏中,每个玩家的共享内存内的数据都必须完全相同,否则会掉线

而非共享内存中的数据可以不相同,不会影响游戏的进行

举例说明

地址0x0057F0F0 (至0x0057F0F3)就是一个共享内存,它储存着的4字节数据就是共享数据,数据内容为玩家1当前的水晶数

假设现在游戏中有玩家1和玩家2两个玩家,现在玩家1的钱数为10,玩家2的钱数为20。那么玩家1的内存0x0057F0F0储存的数据就是10,玩家2的内存0x0057F0F0储存的数据也是10,这两个值完全一样

也就是说,在玩家1眼里,玩家1有10块钱;在玩家2眼里,玩家1也是10块钱,这是大家的共识,这就是共享内存和共享数据

如果在某些触发的影响下,玩家1的内存0x0057F0F0储存的数据变成15,而玩家2的内存0x0057F0F0储存的数据变成20,那么这个时候,两个玩家迅速失去同步(desync),互相显示为对方掉线,两个人分别进入各自的平行宇宙,继续自己独自的游戏

这也就是说,如果游戏中的玩家对于共享数据无法达成共识时,这些玩家将无法继续一起游戏

此外,所有玩家的资源、拥有的单位等等等等,都是共享数据

地址0x006CDDC4就是一个非共享内存,它储存的4字节数据,是当前玩家的鼠标指针所在的横坐标,是非共享数据

假设现在玩家1的鼠标在屏幕左上角,而玩家2的鼠标在屏幕右上角,那么玩家1的0x006CDDC4储存的数据内容就是0,而玩家2的0x006CDDC4中储存的数据内容就是639(假设玩家2用的窄屏)

可以看到,两个玩家在同一个内存地址中储存着不同的内容,游戏依然可以继续

这就是非共享内存和非共享数据。触发可以随便修改非共享内存中的数据,而不会使游戏掉线

此外,玩家的屏幕坐标、聊天显示板的内容、玩家当前按下的键等等等等,也都是非共享数据

共享内存(shared condition)与非共享内存(non-shared condition)

上文说过,触发的本质就是用“条件”来读取内存中的数据,用“动作”来向内存中写入数据

读取的内存可能是shared,也可能是non-share,同理,写入数据的内存也可能是shared或者non-shared

读取共享内存的“条件”,我们称为shared condition,而读取非共享内存的则称为non-shared condition或desync condition

“动作”也是同理,这里不再赘述。那么,触发就可以被分为以下4种:

(1) 读取共享内存(shared condition),写入共享内存(shared action)

(2) 读取共享内存(shared condition),写入非共享内存(non-shared action)

(3) 读取非共享内存(non-shared condition),写入共享内存(shared action)

(4) 读取非共享内存(non-shared condition),写入非共享内存(non-shared action)

其中,第1,2,4种触发都不影响游戏。而只有第3种触发会使游戏掉线,这种触发就是我们所说的掉线触发(desync trigger)

普通的SCMD触发绝大多数都是第1、第2种,所以都不会使游戏掉线。而EUD触发由于可以随意读取内存,也就导致容易写出第3种触发导致掉线

举例说明

在这里要解释一下,每一条触发都是对所有玩家一视同仁的,它都是读取玩家x的内存,并写入玩家x的内存。比如有一个触发是:

拥有者:玩家1

条件:鼠标指针的横坐标大于100(0x006CDDC4储存的数值大于100)

动作:将当前玩家的钱数改为20(读取0x006509B0内存中的内容,记为x,并向内存 "0x0057F0F0+x*4" 内写入数据20)

这个触发的本质其实是:

在这个触发执行前,所有玩家内存中的“当前玩家(0x006509B0)”都会被设为0,即玩家1,然后:

若玩家1的0x006CDDC4储存的数值大于100,则向玩家1的内存0x0057F0F0中写入数据20

若玩家2的0x006CDDC4储存的数值大于100,则向玩家2的内存0x0057F0F0中写入数据20

......

若玩家8的0x006CDDC4储存的数值大于100,则向玩家8的内存0x0057F0F0中写入数据20

那么问题就来了,当这条触发运行的时候,由于0x006CDDC4是个非共享内存,也就是说每个玩家的0x006CDDC4内都可能是不同的内容

即:这个触发的条件对于鼠标位置的横坐标大于100的玩家是成立的,这些玩家的内存0x0057F0F0内就会被写入数据20

而对于其他玩家,条件则是不成立的,所以这条触发的动作就没有给这些玩家执行

这就导致,这条触发运行之后,各个玩家的共享内存0x0057F0F0内储存的数据有差别了,游戏也就掉线了

这条触发就是掉线触发。当然,如果是单人游戏,那么这种触发就完全没有问题

这也是很多地图制作者发现自己的地图在单人模式一切正常,但多人模式会疯狂掉线的原因

注:“触发拥有者”的本质其实也是处理两个非共享内存,“当前玩家编号(0x006509B0)”和“本地玩家编号(0x512684)”,详见另一个教程

总之,无论这个触发的拥有者是谁,这个触发最终都会像上面写的那样,分别作用于每个玩家的内存!

我们基本可以说,凡是在多人游戏中使用了第3种触发,则必掉线

工作原理

在游戏中,我们不断使用鼠标与键盘给游戏单位施加指令,这些指令显然都是非共享数据,是每个玩家自己制造的

那么为何游戏不在一开始就掉线呢?这是因为,系统会把每个玩家私有的这些指令数据(QueueGameCommand)转化为所有玩家共有的“单位指令”数据,然后共享到所有玩家的内存中

比如,玩家1给自己的一个枪兵施加了一个指令“移动到地图(100, 100)的位置”,然后系统就会识别这个指令数据并将其共享给所有玩家

这样,每个玩家的内存中都会被写入“玩家1的那一个枪兵要移动到(100, 100)的位置”,从而使得大家的游戏同步进行

MSQC插件就是巧妙地利用了这一特性:当某个非共享数据被读取时,MSQC会先创造一个“辅助单位(QC Unit)”

将这个非共享数据转化为该玩家对于此单位所施加的指令,这样系统就会顺其自然地将这个指令信息共享给所有玩家,也就实现了“非共享数据”转化为“共享数据”

然后把这个共享数据转化为玩家可以在触发中使用的数据(比如某个单位的死亡数)

这一过程结束后,MSQC会把这个辅助单位从地图上移除

游戏中每扫一轮触发,MSQC就会完成一轮“创建辅助单位、施加指令、数据转化、删除辅助单位”的动作

因此如果想实现非共享内存的实时共享,我们通常要让MSQC配合eudTurbo插件一起使用

由于MSQC是把“非共享数据”转化成了玩家给辅助单位的指令,所以这一过程中,系统会把这一过程误认为是玩家在操作某个单位

所以,使用MSQC的地图中,玩家的APM都会相当高,甚至成千上万

举例说明

举一个例子,比如我们想写一个以“玩家按下键盘上的Q键(非共享数据)”为条件,以某个共享内存作为动作的触发:

这时我们就可以事先告诉MSQC,让它通过上面的方法来把“玩家x是否按下Q键”转化为“玩家x的小狗死亡数”(假设本游戏内用不到小狗这个单位)

也就是说,每当玩家1按下了Q键,MSQC就会把玩家1的小狗死亡数变成1,而当玩家1松开Q键后,MSQC就会把玩家1的小狗死亡数变成0

同理,当玩家2按下Q键的时候,MSQC就会把玩家2的小狗死亡数变成1,以此类推。因此,我们就可以在触发中以“当前玩家的小狗死亡数”为条件 来写触发了

需要强调一点:MSQC将非共享数据转化为共享数据是需要一定的时间的,毕竟在联机游戏中,星际系统识别指令、共享指令都是需要时间的

比如上面的例子,玩家1按下Q键之后,一直到玩家1的小狗死亡数变成1,这之间是有延迟的。经过试验,在无网络延迟的情况下,这个延迟的时间至少为4游戏帧。当网络延迟较高时,则会更长。

MSQC语法

上面说到,我们要事先告诉MSQC,让它把“玩家x是否按下Q键”转化为“玩家x的小狗死亡数”。下面我会讲,如何告诉MSQC让它做这件事,即MSQC的使用语法。

下面的内容会设计到地图的编译。地图的编译需要edd文件(工程文件,类似于makefile),eps文件或者py文件(类似于main)(也称为“插件”),以及输入的scx地图。编译时,要把edd文件送给euddraft.exe进行编译,才能产出成品地图。整个过程其实完全不需要EudEditor。EudEditor仅仅是给上述过程套了一个图形界面而已。对于eps代码的语法我以后有机会会单独开专题来讲,在这里不展开讲了。如有兴趣可以看https://github.com/phu54321/euddraft/wiki/01.-Introduction 自学。

使用MSQC时,主要的代码都要写在edd文件中。下面是一个edd文件的具体实例:


[main]

input: in.scx

output: out.scx

[main.eps]

[MSQC]

MouseDown(L): Goliath Turret, 1

QCDebug : False

[eudTurbo]

所有的edd文件的结构,都是由“插件名称”、“指令”所构成。中括号代表使用的插件名称,后面每一行都由 [key] : [value] 构成,是给这个插件的指令,告诉它要做什么。下面我要介绍的就是MSQC如何来写。下面所有的蓝色斜体,都是要用自己写的代码来代替的。黑色的标点及粗体内容都是要原封不动照搬的内容,我称其为关键词。所有的标点符号都为英文(半角),切记不能用中文标点!

MSQC的语法(syntax)结构:(中括号代表要用自己写的代码来代替)

语法1:


[Non-shared condition 1] ; [Non-shared condition 2] ; [Nonshared condition 3] : [UnitName / UniID / PVariableName], [value to add]

语法2:


Mouse : [LocationName / LocationID]

语法3:


[Non-shared condition 1] ; [Non-shared condition 2] ; Mouse : [LocationName / LocationID]

语法4:


[Non-shared condition 1] ; val, [Address / VariableName] : [UnitName / UniID / PVariableName]

语法5: (第二行不确定,貌似作者写出bug了)


[Non-shared condition 1] ; xy, [Address / VariableName] : [UnitName / UniID / PVariableName]

[Non-shared condition 1] ; xy, [Address / VariableName], [Address / VariableName] : [UnitName / UniID / PVariableName], [UnitName / UniID / PVariableName]

其他语法:

QCDebug : [True / False] (默认True)

QCUnit : [UnitName / UniID / PVariableName] (默认Terran Valkyrie)

QCLoc : [LocationID](默认0)

QCPlayer : [Player ID] (默认10)

QC_XY : [X], [Y] (默认128, 128,单位为像素)

注:

语法1至语法5为基本语法,每一条指令的基本结构都类似于一个触发,冒号左边的内容可以理解为“条件”(val和xy除外),每个条件都用分号隔开,等号右边的可以理解为“动作”,写在冒号右边。

语法4和语法5都必须至少有一个non-shared condition,如果一个都不写就会报错。Non-shared condition数量不限上限,虽然上面的语法讲解中只写了一个,但是你可以自己加任意多个,用分号隔开。

[Non-shared condition]有以下几种写法,其中cmptype可为 AtLeast, Exactly, AtMost中的一个,区分大小写:

(1) [Address], [cmptype], [Value]

例:



0x006CDDC4, AtLeast, 320

意思是“内存0x006CDDC4中的数值等于320”

(2) [VariableName].[cmptype]([Value])

例:x.Exactly(1)。意思是“变量x的值至少为1”。在这种写法中,x为一个EUDVariable,即一个普通变量。此时,需要在eps文件的global环境中先声明这个EUDVariable然后再在onPluginStart()中使用EUDRegisterObjectToNamespace函数来register,具体看使用例。

(3)[key]

(4)KeyDown([key])

(5)KeyUp([key])

(6)KeyPress([key])

(7)MouseDown([L or M or R])

(8)MouseUp([L or M or R])

(9)MousePress([L or M or R])

(10)NotTyping

冒号右边的内容[UnitName / UniID / PVariableName]顾名思义有3种写法:

(1)写UnitName,即单位名,比如Terran Marine,不用加引号

(2)写UnitID,即单位的ID,比如0,代表Terran Marine

(3)写PVariableName,即一个PVariable,即一个EUDArray(8),即一个长度为8的EUDArray的名字,比如y。此时,需要在eps文件的global环境中先声明这个PVariable然后再在onPluginStart()中使用EUDRegisterObjectToNamespace函数来register,具体看使用例。

关于QCUnit等的解释:

QCUnit: 用来进行QC操作的辅助单位,建议使用空军单位。默认瓦格雷

QCLoc: QCUnit要在QCLoc(即QC Location)里面生成。默认0,也就是全图第一个location。所以必须保证全图至少有1个location。

QCPlayer: QCUnit的所属玩家。默认10,即玩家11

QC_XY: 在创建QCUnit时,要把QCLoc移动到某个临时位置,创建完了之后再把QCLoc移到原来的位置。这个XY就是要移到的临时位置。这个位置建议设在空旷的地方,不跟玩家单位发生冲突。默认128,128,即地图左上角的(128,128)像素的位置,即(4,4)格子的右下角位置。

QCDebug: 设为True时,则会在error line上print信息,用来辅助debug。默认True。在作图时建议保持True。全部完成之后确认没有bug了准备给玩家玩的时候,再将QCDebug设为False。

重点注意:在euddraft0.9.1.4中,关键词QCDebug、QCUnit、QCLoc、QCPlayer、QC_XY、KeyDown、KeyUp、KeyPress、MouseDown、MouseUp、MousePress是区分大小写的,一定不能把字母大小写写错。val、xy、mouse、NotTyping是不区分大小写的。

关于其他语法的解释,则直接看使用例

使用例

例1

在main.edd中写入如下内容:


[MSQC]

0x006CDDC4, AtLeast, 320 : 179, 1

0x006CDDC8, AtLeast, 240 : Cave (Unused), 2

效果:

当Memory(0x006CDDC4, AtLeast, 320)条件满足时,将current player的179号Unit的Death数加1,当条件不满足时,就不加1

当Memory(0x006CDDC8, AtLeast, 240)条件满足时,将current player的"Cave (Unused)"的Death数加2,当条件不满足时,就不加2

注:179号unit就是Cave (Unused),这两个写谁都一样,没区别

注:0x006CDDC4储存的是当前玩家鼠标指针的横坐标,0x006CDDC8储存鼠标指针纵坐标。鼠标在屏幕左上角时坐标是(0, 0)

最终效果:(MSQC的精髓:将non-shared condition转化为shared condition)

当current player的鼠标指针位于屏幕左上区域时,他的179号unit的death数被设为0

当current player的鼠标指针位于屏幕右上区域时,他的179号unit的death数被设为1

当current player的鼠标指针位于屏幕左下区域时,他的179号unit的death数被设为2

当current player的鼠标指针位于屏幕右下区域时,他的179号unit的death数被设为3 (有待商榷,见下)

这样的话,我们就可以在触发里以current player的179号unit的death数为条件了。

需要注意,本例子只是对MSQC的基本语法做一个演示。在真正作图的过程中,不建议这么写。因为当MSQC写了有若干条指令时,当两条指令的条件同时满足时,冒号右边的PVariable或DeathUnit的值不一定同时变化。比如说我们这样写MSQC:


[MSQC]

0x57F23C, Exactly, 100; 0x006CDDC4, AtLeast, 320 : 179, 1

0x57F23C, Exactly, 100; 0x006CDDC8, AtLeast, 240 : 179, 2

0x57F23C这个内存储存的是当前游戏的时刻,单位是游戏帧。所以这两行MSQC指令的含义是在t=100游戏帧这个时刻,若P1的鼠标在屏幕右半边,则给P1的179单位加1死亡数;且,若P1的鼠标在屏幕下半边,则给P1的179单位加2死亡数。

我们假设,当t=100帧时,P1的鼠标位于屏幕右下角区域,显然此时“0x006CDDC4, AtLeast, 320”和“0x006CDDC8, AtLeast, 240”两个条件同时满足。经过试验,MSQC在网络条件较好的绝大多数情况下,指令的条件满足时都会稳定地延迟4帧生效。但是在极少数情况下,两条同时满足条件的指令的生效时间可能不同。比如第一个指令可能需要4帧的延迟才能生效,而第二个指令可能需要5帧。所以,在t=104帧时,P1的179号单位的death数为1,而在t=105帧时,P1的179号单位的death数为2。所以在这种情况下,虽然在t=100的时刻P1的鼠标位置在屏幕右下,但是P1的179号单位的death数在任何时刻都不等于3。在这里,由于我的两条指令都加了“0x57F23C, Exactly, 100”这个条件,所以我保证了MSQC只会在t=100的时刻传递1条或2条指令,且二者不互相影响。无论两条指令是否同时生效,我们都可以通过检测t=100帧之后的时刻各个玩家的179号单位的死亡数,来得知t=100时刻各个玩家的鼠标位于屏幕左上、右上、左下、还是右下。

但是,如果不加t=100帧这个条件,那么MSQC就相当于在实时监控各个玩家的鼠标位置,无时无刻不在共享信息。这就会比较麻烦了,比如:在t=2帧时,P1的鼠标在屏幕左下,即此时只有第二个指令满足条件,假设这个指令需要5帧生效,即在t=7帧时生效。假设在t=3帧时,P1的鼠标在屏幕右上,此时只有第一个指令满足条件,假设这个指令需要4帧生效,即也在t=7生效。那么在t=7时,我们就会发现P1的179号单位死亡数为3,然而这个信息并不能反推出之前的时刻里面P1的鼠标位置,这就是一个废的信息。再比如,t=2和t=3时,第二条指令的条件都是满足的,且t=2和t=3两次满足条件之后都在t=7时刻生效,那么t=2时的指令就会被t=3时的指令覆盖掉。

所以说,在使用MSQC时,我们应该:

(1)尽量避免连续不断地共享指令,

(2)尽量避免影响同一个单位/变量的两条指令的条件同时满足。

如果想要实现“左上、右上、左下、右下”这个四元判定,则可以:


[MSQC]

0x006CDDC4, AtMost, 319; 0x006CDDC8, AtMost, 239: 179, 1

0x006CDDC4, AtMost, 319; 0x006CDDC8, AtLeast, 240: 179, 2

0x006CDDC4, AtLeast, 320; 0x006CDDC8, AtMost, 239: 179, 3

0x006CDDC4, AtLeast, 320; 0x006CDDC8, AtLeast, 240: 179, 4

可以发现,上面4条指令的条件是互斥的,不可能有任何两个指令的条件同时被满足。所以,MSQC也就不会在同一时刻执行两条指令了。当然,这种写法仍然是在连续不断地共享指令,因此还需要增加更多的限定条件,来避免不断地执行MSQC指令。

我们可以使用变量来把上面四条指令转化为1条指令,见之后的例子

例2

在main.edd中写入如下内容:


[MSQC]

Mouse : Loc1

含义:

Loc1是一个location的名字,必须要在地图中存在。那么要想让MSQC成功运行,除了必须在地图中的任意位置摆放一个名为"Loc1"的Location之外,还要保证此location之后有至少7个location(假设"Loc1"的Location ID为6, 则ID为7至13的location都必须存在。大小和位置不限)

效果:

假设Loc1的location ID为6,那么这一行代码的效果就是:

将6号location不断移动到player1的鼠标指针位置

将7号location不断移动到player2的鼠标指针位置

将8号location不断移动到player3的鼠标指针位置

...

将13号location不断移动到player8的鼠标指针位置

这个建议配合eudTurbo使用,否则location的移动不流畅

(注:旧版的MurakamiShiinaQC插件要求给每个玩家的鼠标指针都设定一个对应的location,总共要设定8个location。而新版的MSQC语法中,用户仅需要设定玩家1鼠标对应的location即可,剩下的由插件按照上述方式自动设定)

强调:Mouse : Loc1一行代码即可直接设定8个location分别跟踪P1至P8的鼠标位置

例3

在main.edd中写入如下内容:


[MSQC]

A : 0, 1

含义:

当current player按下A键时,将他的Terran Marine的death数加1,否则不加1

注意:写A,等价于写KeyDown(A)

这个“按下A键”的含义是指,当他按下去的一瞬间,检测一次,按住不放的话不起作用。所以用这个的话一定要加上eudTurbo,这样才能顺利检测到。

注:

实际作图时,不建议这么写。建议加上NotTyping条件,见例4

例4


[MSQC]

NotTyping ; A : 0, 1

含义:

当current player未开启聊天框(不处于打字状态)时,并且他按下A键时,将他的Terran Marine的death数加1,否则不加1

注意:写A,等价于写KeyDown(A)

这个“按下A键”的含义是指,当他按下去的一瞬间,检测一次,按住不放的话不起作用。所以用这个的话一定要加上eudTurbo,这样才能顺利检测到。

注:

nottyping不区分大小写

NotTyping条件等价于0x68C144, Exactly, 0

检测键盘按键时,最好要配上NotTyping条件。以免玩家在打字过程中按到目标按键,在无意中触发指令。

键盘上的每一个按键都有对应的写法,比如小键盘数字0是NUMPAD0,左ctrl键是LCTRL,具体见附录

例5


[MSQC]

MouseDown(L) : 0, 1

含义:

当current player按下鼠标左键时,将他的Terran Marine的death数加1,否则不加1

注:MouseDown的检测标准与KeyDown类似。如果要检测鼠标中键或右键,则写MouseDown(M)或MouseDown(R)

例6


[MSQC]

QCUnit : 29

QCLoc : 0

MouseDown(L) : 0, 1

效果:

将QCUnit改为29号单位(Norad II (Battlecruiser)),本来默认的是Terran Valkyrie

第二行就跟没写一样,因为本来默认的QCLoc就是0

第三行见例5

例7

main.edd文件内容:


[main]

input: in.scx

output: out.scx

[main.eps]

[MSQC]

MouseDown(L) : receive, 1

[eudTurbo]

main.eps文件内容:


const rc = PVariable(); //写成rc = EUDArray(8)或者rc = [0, 0, 0, 0, 0, 0, 0, 0]也是一样的
function onPluginStart() {
    EUDRegisterObjectToNamespace("receive", rc); //EUDRegisterObjectToNamespace是函数名,用来register变量
}
function afterTriggerExec() {
    const cp = getcurpl();
    setcurpl(getuserplayerid());
    foreach(p: EUDLoopPlayer("Human")) { //做一次loop,让变量p取遍每一个人类玩家的playerID,
        StringBuffer(256).printfAt(p, "玩家{}是否按下鼠标左键: {}", p+1, rc[p]);
        if (rc[p] == 1)
            CreateUnit(1, "Terran Marine", "Anywhere", p); //为按下鼠标左键的玩家创建一个枪兵
    }   
    setcurpl(cp);
}

本例讲的是如何在MSQC中使用变量。其实,单位的死亡数跟PVariable是完全等价的,所以我们也可以用PVariable来代替单位死亡数。这样会增加代码的可读性,因为我们可以随便给变量起名。

MouseDown(L) : receive, 1 的含义:

当P1按下鼠标左键时,rc[0]的值加1,当P1未按下鼠标左键时,rc[0]的值为0

当P2按下鼠标左键时,rc[1]的值加1,...

...

当P8按下鼠标左键时,rc[7]的值加1,...

其中,receive是在MSQC中使用的变量名,rc是在eps代码中使用的变量名。我们需要通过EUDRegisterObjectToNamespace("receive", rc)来定义这个MSQC变量receive。EUDRegisterObjectToNamespace函数的第一个参数需要带引号,是指MSQC中的变量名,第二个参数不要带引号,是指eps中的变量名。这两个变量本质上是同一个变量,只是变量名字不同而已。注:这个变量在MSQC中的变量名可以跟它在eps中的变量名完全相同。

在实际作图过程中,我推荐使用变量来处理MSQC指令,不推荐使用单位死亡数

例8

main.edd文件内容:


[main]

input: in.scx

output: out.scx

[main.eps]

[MSQC]

0x57F23C, Exactly, 100; send.Exactly(1) : receive, 1

[eudTurbo]

main.eps文件内容:


var send = 0; //eps代码中的所有变量均为私有变量(non-shared variable),即其所在地址为non-shared memory
const receive = PVariable(); //写成receive = EUDArray(8)或者receive = [0, 0, 0, 0, 0, 0, 0, 0]也是一样的
function onPluginStart() {
    EUDRegisterObjectToNamespace("send", send);
    EUDRegisterObjectToNamespace("receive", receive);
}
function afterTriggerExec() {
    if(Memory(0x006CDDC4, AtMost, 319) && Memory(0x006CDDC8, AtMost, 239))
        send = 1; //本地玩家鼠标指针位于屏幕左上区域,将变量send的值设为1
    else
        send = 0; //鼠标不在左上区域时,变量send值设为0
    foreach(p: EUDLoopPlayer("Human")) {
        if (receive[p] == 1)
            CreateUnit(1, "Terran Marine", "Anywhere", p); //为在t=100帧时鼠标在屏幕左上的玩家创建一个枪兵
    }
}

MSQC指令中,使用内存地址和使用变量的语法是有区别的。send.Exactly(1)就是使用变量的值作为条件的语法,其含义等同于Memory(变量send所在的内存地址, Exactly, 1)。很好理解,不再赘述。

本例中,MSQC指令0x57F23C, Exactly, 100; send.Exactly(1) : receive, 1 的直接含义就是:

在t=100帧的时刻,若玩家1内存中的send变量值恰好为1,则将receive[0]的值加1

在t=100帧的时刻,若玩家2内存中的send变量值恰好为1,则将receive[1]的值加1

...

在t=100帧的时刻,若玩家8内存中的send变量值恰好为1,则将receive[7]的值加1

例9

main.edd文件内容:


[main]

input: in.scx

output: out.scx

[main.eps]

[MSQC]

0x57F23C, Exactly, 100; val, send : receive

[eudTurbo]

main.eps文件内容:


var send = 0;
const receive = PVariable();
function onPluginStart() {
    EUDRegisterObjectToNamespace("send", send);
    EUDRegisterObjectToNamespace("receive", receive);
}
function afterTriggerExec() {
    send = 1; //每轮扫触发时都先将send的值设为1
    if(Memory(0x006CDDC4, AtLeast, 320))
        send += 1; //本地玩家鼠标指针位于屏幕右半边时,将变量send的值设加1
    if(Memory(0x006CDDC8, AtLeast, 240))
        send += 2; //本地玩家鼠标指针位于屏幕下半边时,将变量send的值设加2
    foreach(p: EUDLoopPlayer("Human")) {
        if (receive[p] == 1) //玩家p+1在t=100帧时鼠标位于屏幕左上区域
            CreateUnit(1, "Terran Marine", "Anywhere", p);
        if (receive[p] == 2)) //玩家p+1在t=100帧时鼠标位于屏幕右上区域
            CreateUnit(1, "Terran Ghost", "Anywhere", p);
        if (receive[p] == 3)) //玩家p+1在t=100帧时鼠标位于屏幕左下区域
            CreateUnit(1, "Terran Vulture", "Anywhere", p);
        if (receive[p] == 4)) //玩家p+1在t=100帧时鼠标位于屏幕右下区域
            CreateUnit(1, "Terran Goliath", "Anywhere", p);
        }
}

这个例子就是实现【例1】目的的最佳写法,使用val语法来实现功能。val语法的本质就是将某个私有内存所储存的值直接共享。

指令"若干条件; val, send : receive"的作用是:

当P1内存中的情况满足所给条件时,将receive[0]的值赋值为P1内存中send变量的值;当不满足条件时,将receive[0]的值设为-1

当P2内存中的情况满足所给条件时,将receive[1]的值赋值为P2内存中send变量的值;当不满足条件时,将receive[1]的值设为-1

...

当P8内存中的情况满足所给条件时,将receive[7]的值赋值为P8内存中send变量的值;当不满足条件时,将receive[7]的值设为-1

注:

(1)EUD中的所有变量均为u32类型,所以将其值设为-1等同于将其值设为0xFFFFFFFF,即4294967295

(2)send变量的值在一定的取值范围内,MSQC才能正确工作,将其值传递给receive。这个取值范围仅与地图尺寸有关。若地图尺寸(以格为单位)为x×y,则send的取值范围为0至256*x*y-1。比如地图尺寸是128x128,则send的取值范围是0至4194303,即0至0x003FFFFF,这个取值范围相对来讲还是比较大的,所以一般不用担心。但是如果你想传递的值超过了这个范围,就要使用其他方法了

例10

main.edd文件内容:


[main]

input: in.scx

output: out.scx

[main.eps]

[MSQC]

0x57F23C, Exactly, 100; xy, mouseXY_local: mouseXY_rc

[eudTurbo]

main.eps文件内容:


const mouseXaddr = 0x6CDDC4;
const mouseYaddr = 0x6CDDC8;
const mouseXY_rc = PVariable(); //received
const mouseX_rc = PVariable();
const mouseY_rc = PVariable();
var mouseX_local = dwread_epd(EPD(mouseXaddr));
var mouseY_local = dwread_epd(EPD(mouseXaddr));
var mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里
function onPluginStart() {
    EUDRegisterObjectToNamespace("mouseXY_local", mouseXY_local);
    EUDRegisterObjectToNamespace("mouseXY_rc", mouseXY_rc);
}
function afterTriggerExec() {
    mouseX_local = dwread_epd(EPD(mouseXaddr));
    mouseY_local = dwread_epd(EPD(mouseYaddr));
    mouseXY_local = mouseY_local * 0x10000 + mouseX_local; //将本地玩家的鼠标横纵坐标储存在一个变量里,每一轮扫触发都要更新
    foreach (p: EUDLoopPlayer("Human")) {
        mouseY_rc[p], mouseX_rc[p] = div(mouseXY_rc[p], 0x10000); //得到的纵坐标和横坐标分别是mouseXY_rc除以0x10000的商和余数
        //当玩家p+1的MSQC指令未触发时,mouseXY_rc[p]的值为0xFFFFFFFF,此时拆解到的mouseY_rc和mouseX_rc值均为0x0000FFFF
    }
    const cp = getcurpl();
    setcurpl(getuserplayerid());
    foreach(p: EUDLoopPlayer("Human")) {
        StringBuffer(256).printfAt(p, "玩家{}的鼠标指针在其屏幕内的坐标为: {}, {}", p+1, mouseX_rc[p], mouseY_rc[p]);
    }
    setcurpl(cp);
}

以上内容,可以把每个玩家的鼠标坐标通过一个MSQC变量共享给所有人。xy语法的作用和val基本完全相同,也是直接将某个变量的值共享给所有玩家,唯一的不同就是取值范围。xy共享变量的机理是,将被共享的变量拆解为高字节和低字节,各2字节,以这种方式共享。因此,128x128的地图使用MSQC的xy语法所共享的变量取值范围为:被共享的变量的高2字节取值范围为0至0x7FF,低2字节的取值范围也是0x7FF。所以,使用xy共享的变量的最大值为0x07FF07FF。

所以说,xy和val的区别就在于,如果地图尺寸为128x128,两种语法所能共享的变量的取值个数皆为0x400000个值,即4194304个值,

只不过,

val的取值范围为0至0x3FFFFF的所有数值

而xy的取值范围是:高2字节范围为0至0x7FF,低2字节范围为0至0x7FF

读者可以将条件“0x57F23C, Exactly, 100”改为更宽松的条件,比如NotTyping,并观察结果