Skip to content

目标代码

许兴逸 edited this page Feb 28, 2022 · 25 revisions

YukimiScript 二进制文件

YukimiScript 二进制文件是一个小端序RIFF文件,关于RIFF文件详见:资源交换文件格式 (RIFF)

RIFF区块

RIFF区块的四字符识别码为RIFF,读取YukimiScript二进制文件后即可得到此区块,此区块结构如下:

数据 描述
RIFF RIFF区块的四字符识别码
fileType: uint32_t data大小
data 区块数据

其中data的前四字节为四字符识别码YUKI,后面的内容由多个子区块组成,子区块均符合普通区块的格式。

CSTR区块

这个区块中存储了UTF8编码的字符串,每个字符串后都会跟着一个\0表示字符串结尾。
整个YukimiScript二进制文件中仅存在一个CSTR区块。

其他区块访问CSTR区块时,只会提供一个uint32_t,在该区块data开始处向后移动uint32_t个字节即可得到目标字符串的开始地址,从此地址到下一个\0值即为当前访问的字符串。

EXTR区块

这个区块中存储了YukimiScript中用到的外部连接,每个外部连接都占用4字节,data大小除以4即可得到EXTR区块中的元素数量。
每个EXTR区块元素都是一个字符串指针,指向CSTR区块中的一段字符串,该字符串是YukimiScript外部定义的名称。

整个YukimiScript二进制文件中仅存在一个EXTR区块。

SCEN区块

这个区块中存储了YukimiScript中的一个场景。
YukimiScript二进制文件中可能存在多个SCEN区块。

data的前四个字节为CSTR指针,指向CSTR区块中该场景的名称,后续为命令列表。
命令列表中存在多个命令,其中每个命令以如下方式存储:

数据 描述
uint16_t 所调用的命令在EXTR区块中的编号N(即EXTR区块中的data + 4 * N
uint16_t 参数数量
... 多个参数紧密排列

而对于参数则是以下方式存储:

一个参数由4或8字节组成,前四个字节以int32_t读取,并按照以下情况进行处理:

  • 如果为0,则该参数为8字节,后四个字节为int32_t型的int类型参数。
  • 如果为1,则该参数为8字节,后四个字节为float32_t型的real类型参数。
  • 如果大于等于2,则该参数为4字节,当前读到的值减去2即可得到指向CSTR的字符串指针,得到字符串参数。
  • 如果小于等于-1,则该参数为4字节,当前读到的值加上1后取绝对值即可得到只想CSTR的字符串指针,得到Symbol参数。

例子

对以下YukimiScript代码进行编译:


- extern Hello a b c
- extern World a b c

- scene "main"
@Hello 1 2 "World"
@World 1 2 "World"

- scene "foo"
@Hello 1.0 2.0 true

可得到以下格式的字节码:

数据 描述
RIFF RIFF区块的四字符识别码
148: uint32_t RIFF区块的data大小
YUKI RIFF的data部分的前四字节,为一个四字符识别码,表明为一个YukimiScript二进制文件
CSTR CSTR区块的四字符识别码
28: uint32_t CSTR区块中data的大小
Hello\0 字符串Hello
World\0 字符串World
main\0 字符串main
foo\0 字符串foo
true\0\0\0 字符串true,CSTR区块到此结束,额外追加的两个\0是需要将CSTR区块大小对齐到4字节。
EXTR EXTR区块的四字符识别码
4: uint32_t EXTR区块data部分的大小
0: uint32_t Hello外部定义,0为指向其名称"Hello"的CSTR指针
6: uint32_t World外部定义,6为指向其名称"World"的CSTR指针
SCEN SCEN区块的四字符识别码
52: uint32_t 该区块data部分的大小
12: uint32_t 该区块代表的Scene的名称,即指向main的CSTR指针
0: uint16_t 调用第0个外部定义,即Hello
3: uint16_t 传递了三个参数
0: int32_t 第一个参数为int32_t
1: int32_t 参数1
0: int32_t 第二个参数为int32_t
2: int32_t 参数2
8: int32_t 第三个参数大于等于2,为字符串,减去2后得到6,即为指向CSTR的指针,得到字符串“World”。
1: uint16_t 调用了第1个外部定义,即World
3: uint16_t 传递了三个参数
0: int32_t 第一个参数为int32_t
1: int32_t 参数1
0: int32_t 第二个参数为int32_t
2: int32_t 参数2
8: int32_t 第三个参数大于等于2,为字符串,减去2后得到6,即为指向CSTR的指针,得到字符串“World”。
SCEN SCEN区块的四字符识别码
28: uint32_t 此SCEN区块的data大小
17: uint32_t 该区块代表的Scene的名称,即指向foo的CSTR指针
0: uint16_t 调用了外部定义Hello
3: uint16_t 传递了三个参数
1: uint32_t 第一个参数为一个float
1.0: float32_t 参数1.0
1: uint32_t 第二个参数为一个float
2.0: float32_t 参数2.0
-22: uint32_t 第三个参数为一个symbol,加上1后取绝对值得21,即为指向CSTR区块中字符串“true”的指针。

Lua

查看Lua代码生成器

基本结构

编译生成的Lua文件,将会以UTF-8编码,具有以下基本结构:

return function(api) {
  ["Scene1"] = {
    function() --[[操作]] end,
    function() --[[操作]] end,
    function() --[[操作]] end,
  },
  ["Scene2"] = {
  },
  ["Scene3"] = {
  },
  -- 后面存放更多的场景
} end

其中api参数是一个表,里面存放了YukimiScript所需要使用的外部定义和Symbols,后文中称作“api表”。

可以使用Lua的require函数来加载这个文件。
推荐发布时利用构建工具在编译出Lua代码后,再将其编译为Lua字节码形式以降低加载时间。

对Symbol的处理

以下特殊的Symbol将会特殊处理:

  • truefalse会直接按照truefalse进行传递
  • null会被替换为nil
  • nil会直接按照nil进行传递

尽管Lua的Codegen会把nil原样处理,但是建议在YukimiScript中使用null而不是用nil,以保证在其他目标的代码生成器中不会出现行为不一致的问题。

除了以上特殊的Symbol以外,其他的Symbol将会从传入的api表中搜索,如a会被替换为api.a
如果Symbol中存在.,也依然会按照上面的方式处理,如a.b会被替换为api.a.b

对外部定义命令的处理

对于外部定义命令,也会从api表中搜索,但是会把最后一个.替换为:,以传入self参数。
__system_jump,会被替换为api:__system_jump
bg.play,会被替换为api.bg:play

外部定义命令的实现除了要加入self参数外,其余参数的顺序需要和YukimiScript中的参数定义一致,例如以下YukimiScript中定义的外部命令:

- extern bg.play fileName effect

则需要定义如下实现:

api = {}        -- 这里是api表
api.bg = {}     -- 需要在api表中定义bg表,以便于在bg表中查找play命令的实现

function api.bg.play(self, fileName, effect)
    --[[bg.play命令的实现]]
end

如果YukimiScript中编写了如下代码:

- scene "main"
bg.play --effect fade --fileName "a.png"

将会生成以下的Lua代码:

return function(api) {
  ["main"] = {
    function() api.bg:play("a.png", api.fade) end
  },
} end

PyMO

查看Lua代码生成器

PyMO代码生成器需要依赖于libpymo,你可以在代码仓库的根目录中找到它,它用于访问PyMO引擎的各种功能。
在编译YukimiScript代码时,需要将其放到lib中。

关于YukimiScript中Scene编译到PyMO的过程将会进行以下处理:

  1. YukimiScript单个源文件中允许存在一个$init场景,用于在PyMO启动该文件时首先执行这段代码。
  2. YukimiScript中和源文件名相同的场景(如a.ykm中的场景a),将会被强制作为第一个场景使用,并且该场景必须存在。
  3. 除了以上两个场景,其它场景将会被按照YukimiScript中定义的顺序编译到PyMO代码。

libpymo中没有say命令,你可以直接使用YukimiScript中的文本语法。
如果你需要使用角色,那么你可以使用__define_character命令告知代码生成器将某个角色名设置到某个symbol上,之后便可使用这个标记作为角色名,如:

- scene "main"
@__define_character a "角色A"
a:角色A的对话。

将会生成以下代码:

#label main
#say 角色A,角色A的对话。

在某个场景内定义的内容只能在本场景内使用,如果你需要让整个脚本都可用,那么你需要在$init场景中定义角色。
如果你需要让所有脚本均可用,那么你可能需要在lib中定义$init场景。

Clone this wiki locally