Skip to content

目标代码

许兴逸 edited this page Jul 29, 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参数。

DBGS区块

这个区块中存储了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

查看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"]

对外部定义命令的处理

外部定义命令的实现除了要加入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

每个场景中的调试信息表除了FL标明当前场景所在源文件外,余下的每个调试信息记录都与正文代码中的命令调用一一对应。

其中有一些调试信息字段:

  • F 表示被标记的代码的源文件
  • L 行号
  • V 编译期变量表
  • ScopeType 当前被标记代码所在块的类型,可能为macroscene
    • 若为macro,则该区块为宏,且MacroName字段可用,标记场景名称
    • 若为scene,则该区块为场景,且SceneName字段可用,标记场景名称
  • O 递归地表示更上一层的调用栈帧

Json

查看Json代码生成器

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的类型,可能为scenemacro
  • scenescene_typescene,则该项目表示当前场景名称。
  • macroscene_typemacro,则该项目表示当前宏名称。
  • 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

查看Lua代码生成器

PyMO代码生成器需要依赖于libpymo,你可以在CPyMO仓库的根目录中找到它,它用于访问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中导入该宏。

调试信息

在每个指令上方一行或多行都会存放由;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