-
Notifications
You must be signed in to change notification settings - Fork 3
目标代码
注意:目标代码不会保证更新时向前兼容,请勿依赖于目标代码内部格式进行编程。
YukimiScript 二进制文件是一个小端序RIFF文件,关于RIFF文件详见:资源交换文件格式 (RIFF)
RIFF区块的四字符识别码为RIFF
,读取YukimiScript二进制文件后即可得到此区块,此区块结构如下:
数据 | 描述 |
---|---|
RIFF |
RIFF区块的四字符识别码 |
fileType: uint32_t |
data 大小 |
data | 区块数据 |
其中data的前四字节为四字符识别码YUKI
,后面的内容由多个子区块组成,子区块均符合普通区块的格式。
这个区块中存储了UTF8编码的字符串,每个字符串后都会跟着一个\0
表示字符串结尾。
整个YukimiScript二进制文件中仅存在一个CSTR区块。
其他区块访问CSTR区块时,只会提供一个uint32_t,在该区块data开始处向后移动uint32_t个字节即可得到目标字符串的开始地址,从此地址到下一个\0
值即为当前访问的字符串。
这个区块中存储了YukimiScript中用到的外部连接,每个外部连接都占用4字节,data大小除以4即可得到EXTR区块中的元素数量。
每个EXTR区块元素都是一个字符串指针,指向CSTR区块中的一段字符串,该字符串是YukimiScript外部定义的名称。
整个YukimiScript二进制文件中仅存在一个EXTR区块。
这个区块中存储了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中一个场景的调试信息,YukimiScript二进制文件中可能存在多个DBGS区块。
DBGS区块中data部分的前四个字节(uint32_t)指向CSTR区块中该场景的名称字符串,之后四个字节(uint32_t)指向CSTR区块中该场景所属源文件路径字符串,再之后四个字节(uint32_t)表示该场景在源文件中的行号。
之后每条调试信息按照以下格式存储:
- uint32_t 该命令在SCEN区块中的偏移量
- uint32_t 调用栈层数
N
-
N
组以下内容(表示调用栈帧,第一个为当前栈帧,越往后越向上)- uint32_t CSTR区块中文件名的偏移量
- uint32_t 行号
- uint32_t CSTR区块中场景名或宏名的偏移量
- uint32_t 最高位为1表示该栈帧为场景,否则为宏,其余位表示该栈帧中变量数量
M
-
M
组以下内容- uint32_t CSTR中变量名的偏移量
- 值,参见上述“参数”的存储方式
对以下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文件,将会以UTF-8编码,具有以下基本结构:
return function(api) {
["Scene1"] = {
function() --[[操作]] end,
function() --[[操作]] end,
function() --[[操作]] end,
},
["Scene2"] = {
},
["Scene3"] = {
},
-- 后面存放更多的场景
} end
其中api
参数是一个表,里面存放了YukimiScript所需要使用的外部定义和Symbols,后文中称作“api表”。
可以使用Lua的require
函数来加载这个文件。
推荐发布时利用构建工具在编译出Lua代码后,再将其编译为Lua字节码形式以降低加载时间。
以下特殊的Symbol将会特殊处理:
-
true
和false
会直接按照true
和false
进行传递 -
null
会被替换为nil
-
nil
会直接按照nil
进行传递
尽管Lua的Codegen会把nil
原样处理,但是建议在YukimiScript中使用null
而不是用nil
,以保证在其他目标的代码生成器中不会出现行为不一致的问题。
除了以上特殊的Symbol以外,其他的Symbol将会从传入的api
表中搜索,如a
会被替换为api["a"]
。
如果Symbol中存在.
,也依然会按照上面的方式处理,如a["b"]
会被替换为api["a.b"]
。
外部定义命令的实现除了要加入self
参数外,其余参数的顺序需要和YukimiScript中的参数定义一致,例如以下YukimiScript中定义的外部命令:
- extern bg.play fileName effect
则需要定义如下实现:
api = {} -- 这里是api表
api["bg.play"] = function (self, fileName, effect)
--[[bg.play命令的实现]]
end
如果YukimiScript中编写了如下代码:
- extern bg.play effect fileName
- scene "main"
@bg.play --effect fade --fileName "a.png"
将会生成以下的Lua代码:
return function(api) {
["main"] = {
function() api["bg.play"](api, "a.png", api["fade"]) end
},
} end
如果附加了调试信息,则会将上述返回的场景表中增加[0]
,例如:
a.ykm
- extern bg.play effect fileName
- scene "main"
@bg.play --effect fade --fileName "a.png"
将会生成以下的Lua代码:
local s1 = "D:\\Repos\\YukimiScript\\startup.ykm"
local s5 = "a.png"
local s3 = "bg.play"
local s4 = "fade"
local s0 = "main"
local s2 = "scene"
return function(api) return {
[s0] = {
function() api[s3](api, s4, s5) end,
},
[0] = {
[s0] = {
F = s1,
L = 2,
{ F = s1, L = 3, V = {}, ScopeType = s2, SceneName = s0 }
},
}
} end
每个场景中的调试信息表除了F
和L
标明当前场景所在源文件外,余下的每个调试信息记录都与正文代码中的命令调用一一对应。
其中有一些调试信息字段:
-
F
表示被标记的代码的源文件 -
L
行号 -
V
编译期变量表 -
ScopeType
当前被标记代码所在块的类型,可能为macro
或scene
- 若为
macro
,则该区块为宏,且MacroName
字段可用,标记场景名称 - 若为
scene
,则该区块为场景,且SceneName
字段可用,标记场景名称
- 若为
-
O
递归地表示更上一层的调用栈帧
json代码的根是一个数组,里面包含数个场景对象,每个场景对象均拥有以下字段:
-
scene
该场景的名称 -
block
该场景的内容(一个数组) -
debug
调试信息(可能不存在)
其中block
是包含数个调用对象的数组,每个调用对象拥有以下字段:
-
call
被调用命令名称 -
args
参数数组 -
debug
调试信息(可能不存在)
其中对于args
,每个参数为一个JSON值,YukimiScript值与JSON值具有以下对应关系:
YukimiScript值 | JSON值 |
---|---|
true | true |
false | false |
null | null |
其他symbol | 该symbol的字符串 |
字符串 | 字符串 |
int、real | number |
对于debug
对象,这个对象仅在调试版本的目标代码中存在,它将表示一个完整的调用栈,debug
对象包括以下内容:
-
src
字符串,YukimiScript源代码文件所在路径 -
line
number类型,行号 -
scope_type
当前scope的类型,可能为scene
或macro
。 -
scene
若scene_type
为scene
,则该项目表示当前场景名称。 -
macro
若scene_type
为macro
,则该项目表示当前宏名称。 -
var
该层调用栈中存在的变量。 -
outter
若该层是宏,则它将递归地记录上一层的调试信息。
以下是一个示例:
- extern Hello a b c
- extern World a b c
- macro Hello2 a b c
@Hello a b c
- macro World2 a b c
@World a b c
- macro Hello3 a b c
@Hello2 a b c
- scene "main"
@Hello3 1 2 "World"
@World2 1 2 "World"
[
{
"scene": "main",
"debug": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 14,
"scope_type": "scene",
"scene": "main",
"vars": {},
"outter": {}
},
"block": [
{
"call": "Hello",
"args": [
1,
2,
"World"
],
"debug": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 5,
"scope_type": "macro",
"macro": "Hello2",
"vars": {
"a": 1,
"b": 2,
"c": "World"
},
"outter": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 11,
"scope_type": "macro",
"macro": "Hello3",
"vars": {
"a": 1,
"b": 2,
"c": "World"
},
"outter": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 15,
"scope_type": "scene",
"scene": "main",
"vars": {},
"outter": {}
}
}
}
},
{
"call": "World",
"args": [
1,
2,
"World"
],
"debug": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 8,
"scope_type": "macro",
"macro": "World2",
"vars": {
"a": 1,
"b": 2,
"c": "World"
},
"outter": {
"src": "D:\\Repos\\YukimiScript\\startup.ykm",
"line": 16,
"scope_type": "scene",
"scene": "main",
"vars": {},
"outter": {}
}
}
}
]
}
]
PyMO代码生成器需要依赖于libpymo,你可以在CPyMO仓库的根目录中找到它,它用于访问PyMO引擎的各种功能。
在编译YukimiScript代码时,需要将其放到lib中。
关于YukimiScript中Scene编译到PyMO的过程将会进行以下处理:
- YukimiScript单个源文件中允许存在一个$init场景,用于在PyMO启动该文件时首先执行这段代码。
- YukimiScript中和源文件名相同的场景(如
a.ykm
中的场景a
),将会被强制作为第一个场景使用,并且该场景必须存在。 - 除了以上两个场景,其它场景将会被按照YukimiScript中定义的顺序编译到PyMO代码。
libpymo中没有say
命令,你可以直接使用YukimiScript中的文本语法。
如果你需要使用角色,那么你可以使用__define_character
命令告知代码生成器将某个角色名设置到某个symbol上,之后便可使用这个标记作为角色名,如:
- scene "main"
@__define_character a "角色A"
a:角色A的对话。
将会生成以下代码:
#label main
#say 角色A,角色A的对话。
在某个场景内定义的内容只能在本场景内使用,如果你需要让整个脚本都可用,那么你需要在$init
场景中定义角色。
如果你需要让所有脚本均可用,那么你可能需要在lib中定义一个宏自动导入这些名称,之后在每个文件的$init
中导入该宏。
在每个指令上方一行或多行都会存放由;YKMDBG;
开头的调试信息,之后的信息由;
号分割为区块,其中第一个字母表示该区块含义:
-
P - 表示一个字符串,其中
\
会被替换为\\
,换行符会被统一替换为可见字符"\n"
,而对于可见字符子串"\n"
会被替换为"\\n"
,第一次出现则该字符串编号为0,之后依次递增,该区块独占一行,该行后面所有内容均属于该字符串,即使后面出现;
,也视作该P区块的一部分。 -
C - 该区块后续部分表示当前调用的YukimiScript的外部调用名称,后跟P区块中的字符串编号
-
A - 该区块表示YukimiScript外部调用的参数,按顺序传入,该区块第二个字符为类型,后续为该参数的值
-
E - 区块组分隔符
-
L - 行号
-
F - 文件名,后跟P区块中字符串编号
-
S - 当前部分是一个场景,后跟P区块中字符串编号表示场景名
-
M - 当前部分是一个宏,后跟P区块中字符串编号表示宏名
-
V - 在当前区块中存活的常量,它具有类似以下结构:
V<类型标记><名称>=<值>
对于A区块和V区块,其第二个字符表示类型,如:
- AS或VS - 表示一个字符串,后跟P区块中字符串编号
- As或Vs - 表示一个Symbol,后跟P区块中字符串
- Ai或Vi - 表示一个整数
- Ar或Vr - 表示一个实数
多个区块可构成一个组,包括两种组:
-
外部调用标记组 该组由C区块和A区块构成,标记调用了哪个外部调用,并以E结束。
一行调试信息中仅包含一个或零个外部调用标记组。 -
调用栈定位组 该组包括一个L区块、一个F区块、零个或多个V区块、一个S区块或M区块,并以E结束。
注意,S区块和M区块互斥。
该区块将会表示一层调用栈帧,一行调试信息中包含多个调用栈定位组,左边的组表示当前,每向右一个组表示更上一层的调用栈帧。
例子:
- macro a b c d=$"{b}{c}"
@load
- macro b b c
@a b c $"{c}"
- macro c b c
@b b c
- scene "startup"
@c 1 2
产生的含有调试信息的PyMO代码:
;YKMDBG;PD:\\Repos\\YukimiScript\\startup.ykm
;YKMDBG;Pstartup
;YKMDBG;Pload
;YKMDBG;Pnull
;YKMDBG;P2
;YKMDBG;Pa
;YKMDBG;Pb
;YKMDBG;Pc
;YKMDBG;L10;F0;S1
#label SCN_startup
;YKMDBG;C2;As3;E;L2;F0;Vib=1;Vic=2;VSd=4;M5;E;L5;F0;Vib=1;Vic=2;M6;E;L8;F0;Vib=1;Vic=2;M7;E;L11;F0;S1;E
#load