PCF —— 更方便的命令函数

Update log

PCF语法

设计理念

PCF的目标和PCB一样,都是为了让编写命令系统更为方便、可读、简单。

游戏提供的语法其实十分不足,比如只提供了简单的单行注释而没有多行注释,没有提供方便的命令执行统计(即stats)检测等等,调用别的命令函数需要写全名等等。我们希望透过加入一些简单的语法,可以方便编写命令系统,减少模板(Boilerplate)数量,令逻辑更为清晰、可读。此外,我们也提供了一个非常简单,但用途十分强大的穷举系统,极大的方便了命令的编写、修改、阅读。

我们的基础语法就是,每条命令占据一行,特殊语法(多行)则使用()括住内容,其中右括号)必须在独立一行,即前后不能有空格以外的字符。

缩进

缩进即在该行文字前方加上一定数量的空格/Tab,来与别的行进行区分。一般缩进分为不同层级,每层的行前方加入n*c数量的空格/Tab,其中n为层级,c为每层缩进的空格/Tab字符字符数(一个常量,一般使用2/3/4个空格,或1个Tab字符)。如:

无缩进
无缩进
    一层缩进(我们这里每层使用4个空格)
    一层缩进
        两层缩进
无缩进

我们在大部分时候也不强制要求(但我们建议)用户对命令进行缩进,不过在下列情况下用户必须进行缩进:

注释

注释就是不会被游戏或解析器(也就是PCF -> 命令函数的时候)执行的部分,游戏及编译器会忽略掉注释部分的所有字符。

当行以#字符作开始(忽略缩进部分的空白字符),该行会被完全忽略。这是单行注释

如果需要注释大量行数,就可以使用多行注释。开始的一行以/*作开始(忽略缩进部分的空白字符),以结尾为*/的行作终结。两者可以在同一行。

#单行注释
# 但为了漂亮点,我们建议隔一个空格
say # 这是一条命令,尽管有一个#符号,那#符号并不是在行的开始,故此不当作单行注释
    # 这也是注释,因为会忽略掉开始的空格

/*  多行注释
    多行注释
    多行注释  */

注意: 我们不容许使用嵌套多行注释。也就是说/*...*/里面不容许别的多行注释,因为解析器会误把内层的以*/作结尾的行错误认作外层的多行注释的终结。例子:

  /*
    外层注释
    /*
      里层注释
    */
    解析器误以为上一行已经是注释的完结(因为解析器不会检查有没有嵌套注释)。
    故此这里应该是命令,然而其实不是。
  */

命令

命令独占一行。如果之后那些行都是有缩进的话(相对于这行命令),则当作同一条命令处理。只会删掉行开始的空格,行末空格保留

命令并合时会自动加上一个空格,如果不希望加上空格则需要在行末加上\字符。

例子:

say hi
    第二行
    都会当作是同一条命令
say 另外一条命令\
    并合时不会有空格

有时候一开始也会有缩排

if @s[tag=123] (
    say hi
        第二行
      都会当作是同一条命令
          因为是和这条命令的第一行进行比对
    say 另外一条命令
        当然,我们建议用户
        使用一个一致的缩进
)

事件

可以定义一个事件,当玩家的指定判据(Criteria)吻合时调用某个命令函数。实际以进度(Advancement)进行实现。

语法:

event 命令函数名称 (
    YAML,放于进度的Criteria内
)

详细的函数名称语法请参见下方模块部分。

YAML和JSON类似,但更为容易编写,故此我们选择使用YAML。其大致语法为:使用缩进来表达层级关系(如Compound里的东西,List里的东西),使用key:value表达键值关系(和JSON类似,但键不必使用"",字串数值也不必使用"",除非里面有特殊字符),使用-代表列表物品。比如

test:
    bla:
        - a
        - b
        - c
    233: 123
    foo: 
        - test: 1
          test2: 2

等于JSON里的

{
    "test": {
        "bla": [
            "a", 
            "b", 
            "c"
        ], 
        "233": 123, 
        "foo": [
            {
                "test": 1, 
                "test2": 2
            }
        ]
    }
}

可见YAML还是十分方便的,如果需要一些比较深入的语法,可以参见YAML 1.2 语法标准,我们不确定解析器使用的库能准确支援就是了...


Criteria为一个Compound,每个属性都是一个Compound,包括trigger以及conditions。详细请参见进度教程/Wiki。YAML会根据第一行的缩进进行调整,之后的行的缩进会减去第一行的缩进。

# 整段YAML进行缩进
event test:foo (
    ate_apple:
        trigger: minecraft:consume_item
        conditions:
            - Item:
                item: minecraft:stone
                nbt: "{display:{Name:\"测试用石头\"}}"
)

# 或是整段YAML不进行缩进,但内部还是得缩进
event test:foo1 (
ate_apple:
    trigger: minecraft:consume_item
    conditions:
        - Item:
            item: minecraft:stone
            nbt: "{display:{Name:\"测试用石头\"}}"
)

事件的命令函数名称在同一模块里不能重复,因为事件会转为同一名称的进度放在指定的模块内。关于模块的资料请参见之后的模块部分。

命令函数

本格式支持定义多个命令函数,并且使用模块(Module)把它们放在不同的地方方便处理。

模块

模块(Module)定义并且管理不同事件、命令函数。善用模块功能,我们能够令我们的系统更为整齐,方便阅读和修改,以及能够分配工作,增加工作效率。

module 模块名称 (
    模块内容
)

模块名称只能为 英文小写符号、罗马数字字符(0-9)、_-$等字符。我们建议使用_符号分割英语单词,并且不鼓励使用拼音作为模块名称。

模块容许嵌套,即模块内容能够包括别的模块。最外层的模块为命名空间(Namespace),里面每一层模块都可以当作一个文件夹。故此,模块内容许存在别的模块、命令函数、事件。如果命令函数/事件不在模块里,那就默认在system命名空间里。

例子:

module test (
    module say (
        def foo (
            say foo
        )
    )
)

生成结构:

(地图)
└── data
    └── functions
        └── test (命名空间)
            └── say
                └── foo.mcfunction (那个def定义的命令函数)

命令函数名称

注: 这名称并非定义有名字的命令函数(def)时填写的文件名称,而是调用(透过function命令)或引用(透过事件)时的表达方式。

支持两种模式:绝对路径及相对路径。

绝对路径格式为 命名空间:路径,其中路径以/符号作为分割模块的符号并且不包括扩展名,与游戏内一致。比如上方的那foo的名称就是test:say/foo

相对路径格式为相对路径,相对于目前的模块,并且不容许上一层目录。比如我有一个事件在test:里面,我需要调用test:say/foo,格式则为say/foo。相对路径和绝对路径最大分别在于相对路径不能存在命名空间。这格式在同一模块内时特别好用,因为完全不需要理会模块名称,直接使用命令函数的文件名称即可。

system:start基于特殊用途是不容许被调用或被定义的,请各位注意。

定义命令函数

我们可以透过def格式定义一个有名称的命令函数。格式:

def 命令函数文件名称 (
    内容
)

命令函数文件名称和模块名称类似,只能为 英文小写符号、罗马数字字符(0-9)、_-$等字符。我们建议使用_符号分割英语单词,并且不鼓励使用拼音作为文件名称。

注意: 所有命令都必须放在def/act里,即使是之后介绍的匿名命令函数也得写在def/act里。但在def/act里不能定义别的def/act

有时候我们需要特别指定该命令函数由实体执行,这时候我们就需要act。格式为

act 命令函数文件名称 (
    内容
)

注意: system:_init代表初始化的命令,system:main代表高频执行的命令,定义两者只会在其默认命令之后加入新的命令,并且后者(system:main)无法被定义为被实体执行,因为那会被gameLoopFunction游戏规则调用。

我们可以在命令函数内指定执行本命令函数,其命令函数名称为@self,如function @self

act test (
    scoreboard players remove @s score 1
    if @s[score_score_min=1] (
        function @self
    )
)

匿名命令函数

我们定义了四种使用匿名命令函数的方式,分别为 ifunlesscondfail。格式如下:

if 选择器 (
    内容
)
unless 选择器 (
    内容
)
cond (
    内容
)
fail (
    内容
)

并且condfail必须放在act里,因为需要使用实体的命令执行统计。(不用担心绑定stats等问题,我们会为用户处理的。但用户如果使用了condfail不能定义名称为pcfstats的记分板变量(Objective),避免出现冲突问题。)


匿名命令函数必须放在def里或是别的匿名命令函数里。匿名命令函数的内容会放在指定模块内一个随机命令函数里,如$a$1之类,但不建议猜测或寻找指定匿名命令函数的名称,需要调用的话请使用一般的命名函数定义。

注意: 如果要做到类似if else的功能,必须先检查cond后检查failifunless则没有要求,因为没有副作用),因为会如果前者失败了会影响后者(先cond失败了也就代表前面的失败,故此无影响)。但如果是先failcond就会变为成功则完全不执行,失败则全部执行。

常量

提供定义常量功能,文件内的指定关键词(常量名称)将会被首先替换为常量内容,之后才进行别的处理。
定义格式为:

define 常量名称 = 常量内容

其中常量名称一开始为一个$字符,接着就是其名字,建议为 大写英文字母及罗马数字等,以_符号分割字词。常量内容为需要替换为的字符。格式内=符号前后的空格并非必须,可加可不加。

一般会使用常量来统一管理系统参数,如怪物血量、攻击力等等,方便修改。此外,也能使用常量来简化命令,如把一些常用的NBT、命令模式写进常量,之后即可调用。例子:

define $ADD = scoreboard players add
def test (
    say add 1 to score a
    $ADD @p a 1
)

注意: 常量内容前后的空格会被移除。
常量不存在模块的概念,一般是放在文件开始的。

常量内容不建议使用别的常量,因为我们不保证能替换掉那些。

使用常量时,请确保前后都有一个词界(即没有别的字符,或空格、逗号等字符。但不包括中文标点。),这样我们才能辨识为一个常量。

宏类似常量,但能使用参数和编写多行内容。格式为:,

macro 宏名称(参数1, 参数2, 参数3) (
    宏内容
)

每个参数变量以$字符作开始。后方参数数值里可以直接使用参数变量的名称。注意不可以和常量名称重复。

调用时直接使用宏名称(参数1, 参数2, 参数3)即可。和常量类似,使用时请确保前后都有一个空格字符(或其他特殊字符,如换行。但不包括中文标点。),这样我们才能辨识为一个宏。

macro ADD($player, $obj, $delta) (
    scoreboard players add $player $obj $delta
)
def test (
    ADD(@p, test, 1)
)

传递进去的参数不能有,()字符。

调用时的参数,及宏内容能够使用常量。宏的内容容许调用别的宏,但如果因此而发生任何内存不足、堆栈溢出等情况我们不会负责。

如果需要使用计算功能,可以在宏的内容使用for,详细见下。

宏也不存在模块的概念,建议放在文件开始(常量定义之后)。

不能够递归定义宏,宏里生成宏的语法是不会被处理的。

宏内容里每行的缩进都会根据内容的第一行的缩进进行标准化(删掉第一行的缩进)。故此之后每一行的缩进都必须大于第一行的缩进。
在调用时,那些行会根据调用的时候的缩进和标准化后的缩进计算出生成后的缩进(就是加起来)。所以可以使用宏处理YAML等要求严格缩进的格式。

穷举

我们提供了一个简单的穷举系统,但已经足够应付大部分穷举的情况,如穷举角度、二分、计算等。

我们会通过简单循环进行穷举(把里面命令复制多次并且替换指定参数为变量的计算后结果)。循环支持嵌套,格式为

for 变量 from 整数1 to 整数2 (
    命令
)

命令里使用$(表达式)计算数值。表达式支援四则运算、取模(%)、次方(Pow(x, y)为x的y次方)以及部分常用三角函数(Sin, Cos, Tan, Asin, Acos, Atan, Atan2。以角度为单位)。被替换时取小数位3位(如果为小数)。

例子:

for i from 1 to 3 (
    for k from 1 to $(i) (
        say $(i)
    )
)

输出为

# i = 1
    # k = 1
    say 1
# i = 2
    # k = 1
    say 2
    # k = 2
    say 2
# i = 3
    # k = 1
    say 3
    # k = 2
    say 3
    # k = 3
    say 3

注意,变量名称在嵌套部分不能重复。

for模块不需要限制在def里使用,for模块可以生成def模块等不同部分。

简单点说,就是我们会先处理所有常量、宏及for模块,生成文字,然后再去处理相关文字。

由于常量和宏是在for模块之前处理的,故此for模块无法生成常量和宏的定义。

我们可以使用for模块做到二分穷举,比如是我们要穷举玩家在8*8*8的空间的坐标,我们可以这样处理:

act coor (
    # 初始化分数
    scoreboard players set @s x 0
    scoreboard players set @s y 0
    scoreboard players set @s z 0
    # 处理x
    for x from 2 to 0 (
        # 检查 x>4, x>2, x>1的情况,就是 2的x次方,x从2到0
        execute @s ~ ~ ~ scoreboard players add @s[x=$(pow(2,x)),dx=$(pow(2,x))] x $(pow(2,x))
        tp @s[x=$(pow(2,x)),dx=$(pow(2,x))] ~$(-pow(2,x)) ~ ~
    )
    for y from 2 to 0 (
        execute @s ~ ~ ~ scoreboard players add @s[y=$(pow(2,y)),dy=$(pow(2,y))] y $(pow(2,y))
        tp @s[y=$(pow(2,y)),dy=$(pow(2,y))] ~ ~$(-pow(2,y)) ~
    )
    for z from 2 to 0 (
        execute @s ~ ~ ~ scoreboard players add @s[z=$(pow(2,z)),dz=$(pow(2,z))] z $(pow(2,z))
        tp @s[z=$(pow(2,z)),dz=$(pow(2,z))] ~ ~ ~$(-pow(2,z))
    )
)

技术细节

初始化

system:start.mcfunction用于调用初始化模块system:_init。设置gameLoopFunction的命令将会放在system:_init里。

system:start.mcfunction

scoreboard objectives add sys_start dummy
scoreboard players set @s sys_start 0
stats entity @s set QueryResult @s sys_start
gamerule sys_start
function system:_init if @s[score_sys_start=0]
gamerule sys_start 1

system:start.json (进度)

{
    "criteria":{
        "a":{
            "trigger":"minecraft:tick"
        }
    },
    "rewards":{
        "function":"system:start"
    }
}

事件

所有事件生成的进度都是没有显示选项的,故此不会打扰玩家。

输出

直接输出整个data文件夹,用户只需要复制进去地图的data文件夹即可。 注意: 如果要重新初始化,请设置gamerule sys_start 0

所有文件的编码均为UTF-8 without BOM。JSON文件将不会被prettyprint,而是一行的JSON。

命令执行统计

如果有使用任何包括condfail的部分,初始化时则需要加入pcfstats记分板变量,并且在需要统计的命令函数里初始化相关分数及绑定stats。所有相关选择器会使用@s

条件优化

ifunless里只有一条没有条件的命令函数调用,则不需要把内容放进新的命令函数里,只需要转为function ... if/unless 选择器即可。