Repository: Uni-Gal/Ayaka (language: rust)

Issue:
Import other YAML files in config file.
https://github.com/Berrysoft/gal/issues/21

We'd like to consider if the `config.yaml` could include other files. This will make the project easier to organize.

Consider this folder structure:

```
config.yaml
  └─paras
    ├─ja
    │ ├─start.yaml
    │ └─end.yaml
    └─zh-Hans
      ├─start.yaml
      └─end.yaml
```

However, we need to consider these problems:

* Is this structure forced or alternative?
  * If alternative, we need to extend YAML grammar, and it is not that easy.
* When are the files loaded?
  * If they are not all loaded at first, we need to refactor many APIs to async.


Discussion / hints:
我个人觉得非常有必要

（看起来目前尚无其他人，因此就形式从简改采中文做回复了，需要翻译的话我再补）

1. 目前的文件树结构把剧本、元数据和配置信息都混在同一个config文件中，缺乏层次感
2. 对长剧本而言显然不能指望它在一个脚本里就写完（毕竟正常的一部视觉小说怎么说也要三五万字，也才刚刚算小短篇吧），因此允许拆分为start/end等多个文件是必要的，但并非强制要求拆分，部分时候可能也只存在一个文件（但还是应当单独分离出文件去，原因见1，它不应是可选的）
3. 记得要求拆出来的多个脚本，要按语言代码组织文件夹，简中的start.yaml和end.yaml，一定放在zh-Hans文件夹下

---

@Akarinnnnn 也来说说看？可以先看一下这两个目前较为完善的例子

* https://github.com/Uni-Gal/Ayaka/blob/master/examples/Gacha/config.yaml
* https://github.com/Uni-Gal/Ayaka/blob/master/examples/Orga/config.yaml
我想，如果拆分出去，有两种选择：

* 一个文件就是一个 paragraph，类似 Java 一个文件就是一个 class 的要求。这样做允许动态加载，但是考虑到文本几万字也没多大，IO 可能不是瓶颈。也就是说，选择了动态加载对于性能的提升有限，而且如果动态加载的优化不好，反而不如在一开始全读入。
* 不限制文件与 paragraph 的对应关系，加载时读取文件夹下所有的 YAML 文件。这样做只能在一开始读入所有文件。
还有一个思路：引入可见性。一个文件可以有多个 paragraph，但是只有一个是 public，其它的是 private。

例如，对于 start.yaml
``` yaml
- tag: start
  next: foo
- tag: foo
  next: bar
- tag: bar
  next: end
```
以及 end.yaml
``` yaml
- tag: end
  next: foo
- tag: foo
  next: bar
- tag: bar
```

两个文件中都只能看到本文件中的 `foo` 和 `bar`，但是 start.yaml 中也能看到 `end`。

这样就可以允许动态加载，还可以对小文件做预加载的优化。例如小于1M的文件可以预加载到内存，而大文件则可以动态加载。
- 大方向没问题。建议下次更新开始，强制使用新结构。  
- 启动时预加载yaml脚本，进入章节前预加载资源。

Fix patch:
diff --git a/bins/ayaka-check/src/main.rs b/bins/ayaka-check/src/main.rs
index f1a55531..c2d53a4e 100644
--- a/bins/ayaka-check/src/main.rs
+++ b/bins/ayaka-check/src/main.rs
@@ -52,6 +52,9 @@ async fn main() -> Result<()> {
             OpenStatus::LoadPlugin(name, i, len) => {
                 println!("Loading plugin {} ({}/{})", name, i + 1, len)
             }
+            OpenStatus::GamePlugin => println!("Preprocessing game..."),
+            OpenStatus::LoadResource => println!("Loading resources..."),
+            OpenStatus::LoadParagraph => println!("Loading paragraphs..."),
         }
     }
     let mut ctx = context.await?;
diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs
index d911e2fc..23baf29a 100644
--- a/bins/ayaka-gui/src-tauri/src/main.rs
+++ b/bins/ayaka-gui/src-tauri/src/main.rs
@@ -48,10 +48,13 @@ struct FullSettings {
 #[derive(Debug, Clone, Serialize)]
 #[serde(tag = "t", content = "data")]
 enum OpenGameStatus {
-    LoadSettings,
     LoadProfile(String),
     CreateRuntime,
     LoadPlugin(String, usize, usize),
+    GamePlugin,
+    LoadResource,
+    LoadParagraph,
+    LoadSettings,
     LoadGlobalRecords,
     LoadRecords,
     Loaded,
@@ -78,15 +81,18 @@ async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandRes
             OpenStatus::LoadPlugin(name, i, len) => {
                 emit_open_status(&handle, OpenGameStatus::LoadPlugin(name, i, len))?
             }
+            OpenStatus::GamePlugin => emit_open_status(&handle, OpenGameStatus::GamePlugin)?,
+            OpenStatus::LoadResource => emit_open_status(&handle, OpenGameStatus::LoadResource)?,
+            OpenStatus::LoadParagraph => emit_open_status(&handle, OpenGameStatus::LoadParagraph)?,
         }
     }
     let mut ctx = context.await?;
 
     let window = handle.get_window("main").unwrap();
-    window.set_title(&ctx.game.title)?;
+    window.set_title(&ctx.game.config.title)?;
     let settings = {
         emit_open_status(&handle, OpenGameStatus::LoadSettings)?;
-        load_settings(&storage.ident).await.unwrap_or_else(|e| {
+        load_settings(&storage.ident).unwrap_or_else(|e| {
             warn!("Load settings failed: {}", e);
             Settings::new()
         })
@@ -95,17 +101,14 @@ async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandRes
 
     emit_open_status(&handle, OpenGameStatus::LoadGlobalRecords)?;
     ctx.set_global_record(
-        load_global_record(&storage.ident, &ctx.game.title)
-            .await
-            .unwrap_or_else(|e| {
-                warn!("Load global records failed: {}", e);
-                Default::default()
-            }),
+        load_global_record(&storage.ident, &ctx.game.config.title).unwrap_or_else(|e| {
+            warn!("Load global records failed: {}", e);
+            Default::default()
+        }),
     );
 
     emit_open_status(&handle, OpenGameStatus::LoadRecords)?;
-    *storage.records.lock().await = load_records(&storage.ident, &ctx.game.title)
-        .await
+    *storage.records.lock().await = load_records(&storage.ident, &ctx.game.config.title)
         .unwrap_or_else(|e| {
             warn!("Load records failed: {}", e);
             Default::default()
@@ -162,10 +165,10 @@ async fn save_record_to(index: usize, storage: State<'_, Storage>) -> CommandRes
 #[command]
 async fn save_all(storage: State<'_, Storage>) -> CommandResult<()> {
     if let Some(context) = storage.context.lock().await.as_ref() {
-        let game = &context.game.title;
-        save_settings(&storage.ident, context.settings()).await?;
-        save_global_record(&storage.ident, game, context.global_record()).await?;
-        save_records(&storage.ident, game, &storage.records.lock().await).await?;
+        let game = &context.game.config.title;
+        save_settings(&storage.ident, context.settings())?;
+        save_global_record(&storage.ident, game, context.global_record())?;
+        save_records(&storage.ident, game, &storage.records.lock().await)?;
     }
     Ok(())
 }
@@ -206,9 +209,9 @@ struct GameInfo {
 impl GameInfo {
     pub fn new(game: &Game) -> Self {
         Self {
-            title: game.title.clone(),
-            author: game.author.clone(),
-            props: game.props.clone(),
+            title: game.config.title.clone(),
+            author: game.config.author.clone(),
+            props: game.config.props.clone(),
         }
     }
 }
diff --git a/bins/ayaka-gui/src/interop/index.ts b/bins/ayaka-gui/src/interop/index.ts
index 1701b2cd..1f24e0dd 100644
--- a/bins/ayaka-gui/src/interop/index.ts
+++ b/bins/ayaka-gui/src/interop/index.ts
@@ -17,6 +17,9 @@ export enum OpenGameStatusType {
     LoadProfile,
     CreateRuntime,
     LoadPlugin,
+    GamePlugin,
+    LoadResource,
+    LoadParagraph,
     LoadSettings,
     LoadGlobalRecords,
     LoadRecords,
diff --git a/bins/ayaka-gui/src/views/StartView.vue b/bins/ayaka-gui/src/views/StartView.vue
index e00e3111..559d63a3 100644
--- a/bins/ayaka-gui/src/views/StartView.vue
+++ b/bins/ayaka-gui/src/views/StartView.vue
@@ -61,7 +61,7 @@ export default {
             }
         },
         status_to_text(s: OpenGameStatus): [string, number] {
-            const step = 100 / 7
+            const step = 100 / 10
             const t = OpenGameStatusType[s.t]
             switch (t) {
                 case OpenGameStatusType.LoadProfile:
@@ -71,6 +71,12 @@ export default {
                 case OpenGameStatusType.LoadPlugin:
                     const data = s.data as unknown as [string, number, number]
                     return [`Loading plugin ${data[0]}...`, step * (t + 1) + data[1] / data[2] * step]
+                case OpenGameStatusType.GamePlugin:
+                    return ["Preprocessing game...", step * (t + 1)]
+                case OpenGameStatusType.LoadResource:
+                    return ["Loading resources...", step * (t + 1)]
+                case OpenGameStatusType.LoadParagraph:
+                    return ["Loading paragraphs...", step * (t + 1)]
                 case OpenGameStatusType.LoadSettings:
                     return ["Loading settings...", step * (t + 1)]
                 case OpenGameStatusType.LoadGlobalRecords:
diff --git a/bins/ayaka-latex/src/main.rs b/bins/ayaka-latex/src/main.rs
index c6e9827d..cadae466 100644
--- a/bins/ayaka-latex/src/main.rs
+++ b/bins/ayaka-latex/src/main.rs
@@ -31,8 +31,8 @@ async fn main() -> Result<()> {
     output.command("usepackage", ["lua-ul"]).await?;
     output.command("usepackage", ["luatexja-ruby"]).await?;
     output.command("usepackage", ["verbatim"]).await?;
-    output.command("title", [&ctx.game.title]).await?;
-    output.command("author", [&ctx.game.author]).await?;
+    output.command("title", [&ctx.game.config.title]).await?;
+    output.command("author", [&ctx.game.config.author]).await?;
     output
         .environment("document", |output| async move {
             output.command0("maketitle").await?;
diff --git a/book/src/config/resources.md b/book/src/config/resources.md
index 5c96399e..63030e5b 100644
--- a/book/src/config/resources.md
+++ b/book/src/config/resources.md
@@ -1,27 +1,29 @@
 # Resources
 The resources are indexed by locale:
-``` yaml
-res:
-  en:
-    foo: Foo value
-    bar: Bar value
+``` ignore
+config.yaml
+└─res
+  ├─en.yaml
+  ├─ja.yaml
+  └─zh.yaml
 ```
 You can specify other locales, too.
 The keys not specified in other locales will fallback to `base_lang` ones.
+
+`en.yaml`
+``` yaml
+foo: Foo
+bar: Bar
+```
+`zh.yaml`
 ``` yaml
-base_lang: en
-res:
-  en:
-    foo: Foo
-    bar: Bar
-  zh:
-    foo: 天
-    bar: 地
+foo: 天
+bar: 地
 ```
 
 ## Reference resources
 You can reference resources in texts with `\res{}` command.
 ``` yaml
-- 'The foo value: \res{foo}'
-- 'The bar value: \res{bar}'
+- The foo value is \res{foo}
+- The bar value is \res{bar}
 ```
diff --git a/book/src/config/structure.md b/book/src/config/structure.md
index 9447f73a..cbd619c4 100644
--- a/book/src/config/structure.md
+++ b/book/src/config/structure.md
@@ -1,18 +1,34 @@
 # File structure
 
+## Directory structure
+``` ignore
+config.yaml
+├─paras
+│ ├─ja
+│ │ ├─start.yaml
+│ │ └─end.yaml
+│ └─zh-Hans
+│   ├─start.yaml
+│   └─end.yaml
+└─res
+  ├─ja.yaml
+  └─zh-Hans.yaml
+```
+
 ## Properties
-The total config file is a `Game` object.
+The total config file is a `GameConfig` object.
 Here shows the properties:
 
-| Property    | Description                                 |
-| ----------- | ------------------------------------------- |
-| `title`     | The title of the game.                      |
-| `base_lang` | The base language.                          |
-| `paras`     | The `Paragraph` objects, indexed by locale. |
-| `author`    | Optional. The author of the game.           |
-| `plugins`   | Optional. The `PluginConfig` object.        |
-| `res`       | Optional. The resources, indexed by locale. |
-| `props`     | Optional. The custom properties.            |
+| Property    | Description                          |
+| ----------- | ------------------------------------ |
+| `title`     | The title of the game.               |
+| `base_lang` | The base language.                   |
+| `paras`     | The paragraph path.                  |
+| `start`     | The start paragraph.                 |
+| `author`    | Optional. The author of the game.    |
+| `plugins`   | Optional. The `PluginConfig` object. |
+| `res`       | Optional. The resource path.         |
+| `props`     | Optional. The custom properties.     |
 
 The `PluginConfig` object contains the base directory and the plugin names:
 
@@ -30,21 +46,59 @@ A `Paragraph` object is a collection of texts:
 | `title`  | Optional. The title of the paragraph. |
 | `next`   | Optional. The next paragraph.         |
 
+## The visibility of paragraphs
+Only the paragraph whose tag is the same as the file name(without extension) is public to all paragraphs.
+The rest paragraphs in this file could only be referenced by the paragraphs in the same file.
+
+For example, for `start.yaml`
+``` ignore
+- tag: start
+  next: foo
+- tag: foo
+  next: bar
+- tag: bar
+  next: end
+```
+and `end.yaml`
+``` ignore
+- tag: end
+  next: foo
+- tag: foo
+  next: bar
+- tag: bar
+```
+
+The `foo` and `bar` referenced are the ones in the same file, while `start` and `end` could be referenced from other files.
+
 ## Basic example
 This is a config example, with 2 paragraphs.
+``` ignore
+config.yaml
+└─paras
+  └─en
+    ├─para1.yaml
+    └─para2.yaml
+```
+`config.yaml`
 ``` yaml
 title: Title
 base_lang: en
-paras:
-  en:
-    - tag: para1
-      texts:
-        - This is the first line.
-        - This is the second line.
-      next: para2
-    - tag: para2
-      texts:
-        - The first line of the second paragraph.
+paras: paras
+start: para1
+```
+`para1.yaml`
+``` yaml
+- tag: para1
+  texts:
+    - This is the first line.
+    - This is the second line.
+  next: para2
+```
+`para2.yaml`
+``` yaml
+- tag: para2
+  texts:
+    - The first line of the second paragraph.
 ```
 The output will be
 ``` ignore
diff --git a/examples/Fibonacci/config.yaml b/examples/Fibonacci/config.yaml
index e1199948..0ea92b45 100644
--- a/examples/Fibonacci/config.yaml
+++ b/examples/Fibonacci/config.yaml
@@ -1,14 +1,5 @@
 title: Fibonacci
 author: Berrysoft
 base_lang: en
-paras:
-  en:
-    - tag: init
-      texts:
-        - 1
-        - \exec{$n = 50; $a = 1; $b = 1; $i = 1; $b}
-      next: loop
-    - tag: loop
-      texts:
-        - \exec{c = $b; $b += $a; $a = c; $i += 1; $b}
-      next: \exec{if($i < $n, "loop")}
+paras: paras
+start: init
diff --git a/examples/Fibonacci/paras/en/init.yaml b/examples/Fibonacci/paras/en/init.yaml
new file mode 100644
index 00000000..b7a99cd6
--- /dev/null
+++ b/examples/Fibonacci/paras/en/init.yaml
@@ -0,0 +1,9 @@
+- tag: init
+  texts:
+    - 1
+    - \exec{$n = 50; $a = 1; $b = 1; $i = 1; $b}
+  next: loop
+- tag: loop
+  texts:
+    - \exec{c = $b; $b += $a; $a = c; $i += 1; $b}
+  next: \exec{if($i < $n, "loop")}
diff --git a/examples/Fibonacci2/config.yaml b/examples/Fibonacci2/config.yaml
index 1add16f8..816bcf21 100644
--- a/examples/Fibonacci2/config.yaml
+++ b/examples/Fibonacci2/config.yaml
@@ -6,13 +6,5 @@ plugins:
     - random
     - format
 base_lang: en
-paras:
-  en:
-    - tag: init
-      texts:
-        - \exec{$n = random.rnd(50); $a = 1; $b = 1; $i = 1; format.fmt("Starting to calculate Fibonacci {}", $n)}
-      next: loop
-    - tag: loop
-      texts:
-        - \exec{a = $a; b = $b; c = $b; $b += $a; $a = c; $i += 1; format.fmt("{:<11} + {:<11} = {:<11}", a, b, $b)}
-      next: \exec{if($i < $n, "loop")}
+paras: paras
+start: init
diff --git a/examples/Fibonacci2/paras/en/init.yaml b/examples/Fibonacci2/paras/en/init.yaml
new file mode 100644
index 00000000..03546d76
--- /dev/null
+++ b/examples/Fibonacci2/paras/en/init.yaml
@@ -0,0 +1,4 @@
+- tag: init
+  texts:
+    - \exec{$n = random.rnd(50); $a = 1; $b = 1; $i = 1; format.fmt("Starting to calculate Fibonacci {}", $n)}
+  next: loop
diff --git a/examples/Fibonacci2/paras/en/loop.yaml b/examples/Fibonacci2/paras/en/loop.yaml
new file mode 100644
index 00000000..5d6d81a5
--- /dev/null
+++ b/examples/Fibonacci2/paras/en/loop.yaml
@@ -0,0 +1,4 @@
+- tag: loop
+  texts:
+    - \exec{a = $a; b = $b; c = $b; $b += $a; $a = c; $i += 1; format.fmt("{:<11} + {:<11} = {:<11}", a, b, $b)}
+  next: \exec{if($i < $n, "loop")}
diff --git a/examples/Gacha/config.yaml b/examples/Gacha/config.yaml
index 6eb07aa8..2d3cf8bc 100644
--- a/examples/Gacha/config.yaml
+++ b/examples/Gacha/config.yaml
@@ -10,108 +10,5 @@ plugins:
 props:
   videos: videos
 base_lang: zh-Hans
-paras:
-  zh-Hans:
-    - tag: init
-      texts:
-        - \exec{$total = 0; $t3 = 0; $t4 = 0; $t5 = 0; $c4 = 0; $c5 = 0;}
-      next: start
-    - tag: start
-      texts:
-        - |
-          \exec{format.fmt("你共抽了{}次，三星{}个，四星{}个，五星{}个", $total, $t3, $t4, $t5)}
-          \switch{祈愿一次}{$n = 1}
-          \switch{祈愿十次}{$n = 10}
-          \switch{退出}{$n = -1}
-      next: |
-        \exec{
-          $total5 = 0;
-          $total4 = 0;
-          $total3 = 0;
-          $temp5 = $c5;
-          $temp4 = $c4;
-          if($n > 0, "calc")
-        }
-    - tag: calc
-      texts:
-        - |
-          \exec{
-            p = random.rnd(1000);
-            current5 = if($temp5 < 73,
-              p < 6,
-              if($temp5 >= 99,
-                true,
-                p < (6 + 60 * ($temp5 - 73))
-              )
-            );
-            current4 = if($temp4 < 8,
-              p < 51,
-              if($temp4 >= 9,
-                true,
-                p < 561
-              )
-            );
-            if(current5,
-              $total5 += 1,
-              if(current4,
-                $total4 += 1,
-                $total3 += 1
-              )
-            );
-            if(current5,
-              $temp4 += 1,
-              if(current4,
-                $temp4 = 0,
-                $temp4 += 1
-              )
-            );
-            if(current5,
-              $temp5 = 0,
-              $temp5 += 1,
-            );
-            ayalog.info(if(current5, 5, if(current4, 4, 3)), " star");
-          }
-      next: |
-        \exec{
-          $n -= 1;
-          if($n,
-            "calc",
-            if($total3 + $total4 + $total5 == 1,
-              if($total5,
-                "pull2",
-                if($total4,
-                  "pull1",
-                  "pull0"
-                )
-              ),
-              if($total5,
-                "pull4",
-                "pull3"
-              )
-            )
-          )
-        }
-    - tag: pull0
-      texts:
-        - \video{0}\exec{$total += 1;}
-      next: end
-    - tag: pull1
-      texts:
-        - \video{1}\exec{$total += 1;}
-      next: end
-    - tag: pull2
-      texts:
-        - \video{2}\exec{$total += 1;}
-      next: end
-    - tag: pull3
-      texts:
-        - \video{3}\exec{$total += 10;}
-      next: end
-    - tag: pull4
-      texts:
-        - \video{4}\exec{$total += 10;}
-      next: end
-    - tag: end
-      texts:
-        - \exec{$t5 += $total5; $t4 += $total4; $t3 += $total3; $c4 = $temp4; $c5 = $temp5;}
-      next: start
+paras: paras
+start: init
diff --git a/examples/Gacha/paras/zh-Hans/init.yaml b/examples/Gacha/paras/zh-Hans/init.yaml
new file mode 100644
index 00000000..7b286c3a
--- /dev/null
+++ b/examples/Gacha/paras/zh-Hans/init.yaml
@@ -0,0 +1,103 @@
+- tag: init
+  texts:
+    - \exec{$total = 0; $t3 = 0; $t4 = 0; $t5 = 0; $c4 = 0; $c5 = 0;}
+  next: start
+- tag: start
+  texts:
+    - |
+      \exec{format.fmt("你共抽了{}次，三星{}个，四星{}个，五星{}个", $total, $t3, $t4, $t5)}
+      \switch{祈愿一次}{$n = 1}
+      \switch{祈愿十次}{$n = 10}
+      \switch{退出}{$n = -1}
+  next: |
+    \exec{
+      $total5 = 0;
+      $total4 = 0;
+      $total3 = 0;
+      $temp5 = $c5;
+      $temp4 = $c4;
+      if($n > 0, "calc")
+    }
+- tag: calc
+  texts:
+    - |
+      \exec{
+        p = random.rnd(1000);
+        current5 = if($temp5 < 73,
+          p < 6,
+          if($temp5 >= 99,
+            true,
+            p < (6 + 60 * ($temp5 - 73))
+          )
+        );
+        current4 = if($temp4 < 8,
+          p < 51,
+          if($temp4 >= 9,
+            true,
+            p < 561
+          )
+        );
+        if(current5,
+          $total5 += 1,
+          if(current4,
+            $total4 += 1,
+            $total3 += 1
+          )
+        );
+        if(current5,
+          $temp4 += 1,
+          if(current4,
+            $temp4 = 0,
+            $temp4 += 1
+          )
+        );
+        if(current5,
+          $temp5 = 0,
+          $temp5 += 1,
+        );
+        ayalog.info(if(current5, 5, if(current4, 4, 3)), " star");
+      }
+  next: |
+    \exec{
+      $n -= 1;
+      if($n,
+        "calc",
+        if($total3 + $total4 + $total5 == 1,
+          if($total5,
+            "pull2",
+            if($total4,
+              "pull1",
+              "pull0"
+            )
+          ),
+          if($total5,
+            "pull4",
+            "pull3"
+          )
+        )
+      )
+    }
+- tag: pull0
+  texts:
+    - \video{0}\exec{$total += 1;}
+  next: end
+- tag: pull1
+  texts:
+    - \video{1}\exec{$total += 1;}
+  next: end
+- tag: pull2
+  texts:
+    - \video{2}\exec{$total += 1;}
+  next: end
+- tag: pull3
+  texts:
+    - \video{3}\exec{$total += 10;}
+  next: end
+- tag: pull4
+  texts:
+    - \video{4}\exec{$total += 10;}
+  next: end
+- tag: end
+  texts:
+    - \exec{$t5 += $total5; $t4 += $total4; $t3 += $total3; $c4 = $temp4; $c5 = $temp5;}
+  next: start
diff --git a/examples/Live2D/config.yaml b/examples/Live2D/config.yaml
index 04489973..24e22836 100644
--- a/examples/Live2D/config.yaml
+++ b/examples/Live2D/config.yaml
@@ -6,23 +6,11 @@ props:
   ch_Haru_scale: 0.25
   ch_Hiyori_scale: 0.2
   ch_Mao_scale: 0.1
-res:
-  ja:
-    ch_Haru: はる
-    ch_Hiyori: ひより
-    ch_Mao: まお
+res: res
 plugins:
   dir: ../plugins/
   modules:
     - live2d
 base_lang: ja
-paras:
-  ja:
-    - tag: show
-      texts:
-        - /Haru//おはよう\show
-        - /Haru//元気ですか
-        - /Hiyori//~~~ヾ(＾∇＾)おはよー♪\show{Haru}{Hiyori}
-        - /Hiyori//今日もよろしく\hide{Haru}
-        - /Mao/？？？/お早う\hide
-        - /Mao//元気？\show
+paras: paras
+start: show
diff --git a/examples/Live2D/paras/ja/show.yaml b/examples/Live2D/paras/ja/show.yaml
new file mode 100644
index 00000000..acea9404
--- /dev/null
+++ b/examples/Live2D/paras/ja/show.yaml
@@ -0,0 +1,8 @@
+- tag: show
+  texts:
+    - /Haru//おはよう\show
+    - /Haru//元気ですか
+    - /Hiyori//~~~ヾ(＾∇＾)おはよー♪\show{Haru}{Hiyori}
+    - /Hiyori//今日もよろしく\hide{Haru}
+    - /Mao/？？？/お早う\hide
+    - /Mao//元気？\show
diff --git a/examples/Live2D/res/ja.yaml b/examples/Live2D/res/ja.yaml
new file mode 100644
index 00000000..3e462fbb
--- /dev/null
+++ b/examples/Live2D/res/ja.yaml
@@ -0,0 +1,3 @@
+ch_Haru: はる
+ch_Hiyori: ひより
+ch_Mao: まお
diff --git a/examples/Orga/config.yaml b/examples/Orga/config.yaml
index c6507fa0..0f315925 100644
--- a/examples/Orga/config.yaml
+++ b/examples/Orga/config.yaml
@@ -5,127 +5,12 @@ props:
   bgs: bgs
   bgms: bgms
   voices: voices
-res:
-  ja:
-    ch_rd: ライド
-    ch_org: オルガ
-    ch_kdr: クーデリア
-    ch_tkk: タカキ
-    ch_td: チャド
-    ch_ass: 暗殺者
-    ch_mktk: 三日月
-  zh-Hans:
-    ch_rd: 莱德
-    ch_org: 奥尔加
-    ch_kdr: 古荻莉亚
-    ch_tkk: 塔卡基
-    ch_td: 查德
-    ch_ass: 刺客
+res: res
 plugins:
   dir: ../plugins/
   modules:
     - basictex
     - media
 base_lang: ja
-paras:
-  ja:
-    - tag: pre_car
-      title: 車の用意し
-      texts:
-        - |
-          \ch{rd}団長！車の用意できました！\bg{0}
-          \switch{おう！}{$end = false}
-          \switch{止まるんじゃねぇぞ！}{$end = true}
-          \switch{止まれ！}{}{false}
-      next: \exec{if($end, "end", "walk")}
-    - tag: walk
-      title: 静かだ
-      texts:
-        - \ch{org}おう！じゃあな。
-        - /kdr//はい、お気をつけて。
-        - |
-          /rd//なんか静かですね。街の中にはギャラルホルンもいないし本部とはえらい違いだ。\bg{1}
-          \switch{安全}{}
-          \switch{気を付け}{}{false}
-        - /org//ああ。火星の戦力は軒並み向こうに回してんのかもな。
-        - /rd//まっそんなのもう関係ないですけどね！
-        - /org//上機嫌だな。
-        - /rd/ガンバラナイト/そりゃそうですよ！みんな助かるし、\res{ch_tkk}も頑張ってたし、俺も\ruby{頑張らないと}{ガンバラナイト}！
-        - /org//ああ。
-        - そうだ。俺たちが今まで積み上げてきたもんは全部無駄じゃなかった。これからも俺たちが立ち止まらないかぎり道は続く。
-      next: gun
-    - tag: gun
-      title: 何やってんだよ
-      texts:
-        - /td//ぐわっ！\bg{2}
-        - /rd//団長？何やってんだよ？団長！
-        - /org//ぐっ！うおぉ～～！
-        - /ass//うおっ！あっ！
-        - /org//はぁはぁはぁ……なんだよ、結構当たんじゃねぇか。ふっ……\bg{3}
-        - /rd//だ……団長……あっ……あぁ……
-        - /org//なんて声出してやがる……ライド。
-        - /rd//だって……だって……\bgm{0}
-        - /org//俺は鉄華団団長オルガ・イツカだぞ。こんくれぇなんてこたぁねぇ。
-        - /rd//そんな……俺なんかのために……
-        - /org//団員を守んのは俺の仕事だ。
-        - /rd//でも！
-        - /org//いいから行くぞ。皆が待ってんだ。それに……。
-        - ミカ、やっと分かったんだ。
-        - 俺たちにはたどりつく場所なんていらねぇ。ただ進み続けるだけでいい。止まんねぇかぎり、道は続く。
-        - /mktk//謝ったら許さない。
-        - /org//ああ分かってる。
-      next: end
-    - tag: end
-      title: 止まるんじゃねぇぞ
-      texts:
-        - /org//俺は止まんねぇからよ、お前らが止まんねぇかぎり、その先に俺はいるぞ！\bgm{0}\bg{4}
-        - /org//だからよ、止まるんじゃねぇぞ……
-  zh-Hans:
-    - tag: pre_car
-      title: 备车
-      texts:
-        - |
-          团长！车已经准备好了！
-          \switch{哦！}
-          \switch{不要停下来啊！}
-          \switch{停下！}
-    - tag: walk
-      title: 安静
-      texts:
-        - 哦！那么我走了。
-        - 好，路上小心。
-        - |
-          感觉真安静啊，街上也没有加拉尔霍恩的人，和总部差别真大啊。
-          \switch{安全}
-          \switch{小心}
-        - 啊，可能是他们把火星的战力都调到那里去了。
-        - 不过这也和我们没关系了。
-        - 心情很愉快啊。
-        - /rd/加把劲骑士/那是当然的了！大家也有救了，{}又那么努力，我也\ruby{要加油了}{是加把劲骑士}！
-        - 啊。
-        - 没错，我们至今为止所做的一切，并不是全部徒劳的。今后也是，只要我们不停下脚步，道路就会不断延伸。
-    - tag: gun
-      title: 你在干什么啊
-      texts:
-        - 咕哇！
-        - 团长，你在干什么啊，团长！
-        - 呃呃啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊！！！
-        - 呜哦！啊！
-        - 哈哈哈……什么嘛，我的枪法还挺准的嘛。呵……
-        - 团……团长……啊……啊……
-        - 你的声音为什么要颤抖，莱德。
-        - 因为……因为……
-        - 我可是铁华团团长奥尔加·伊兹卡啊，这点小伤无关紧要。
-        - 为什么……要为了保护我……
-        - 保护团员就是我的使命。
-        - 但是！
-        - 好了，快走吧！大家都在等着你们。而且……
-        - 三日，我终于明白了。
-        - 我们的目的地根本不重要，只要继续前进就好了，只要不停下脚步，道路就会不断延伸。
-        - 道歉的话我是不会原谅你的。
-        - 啊，我明白。
-    - tag: end
-      title: 不要停下来啊
-      texts:
-        - 我是不会停下来的，只要你们不停下来，那前面就一定有我！
-        - 所以，不要停下来啊……
+paras: paras
+start: main
diff --git a/examples/Orga/paras/ja/main.yaml b/examples/Orga/paras/ja/main.yaml
new file mode 100644
index 00000000..8304556b
--- /dev/null
+++ b/examples/Orga/paras/ja/main.yaml
@@ -0,0 +1,51 @@
+- tag: main
+  title: 車の用意し
+  texts:
+    - |
+      \ch{rd}団長！車の用意できました！\bg{0}
+      \switch{おう！}{$end = false}
+      \switch{止まるんじゃねぇぞ！}{$end = true}
+      \switch{止まれ！}{}{false}
+  next: \exec{if($end, "end", "walk")}
+- tag: walk
+  title: 静かだ
+  texts:
+    - \ch{org}おう！じゃあな。
+    - /kdr//はい、お気をつけて。
+    - |
+      /rd//なんか静かですね。街の中にはギャラルホルンもいないし本部とはえらい違いだ。\bg{1}
+      \switch{安全}{}
+      \switch{気を付け}{}{false}
+    - /org//ああ。火星の戦力は軒並み向こうに回してんのかもな。
+    - /rd//まっそんなのもう関係ないですけどね！
+    - /org//上機嫌だな。
+    - /rd/ガンバラナイト/そりゃそうですよ！みんな助かるし、\res{ch_tkk}も頑張ってたし、俺も\ruby{頑張らないと}{ガンバラナイト}！
+    - /org//ああ。
+    - そうだ。俺たちが今まで積み上げてきたもんは全部無駄じゃなかった。これからも俺たちが立ち止まらないかぎり道は続く。
+  next: gun
+- tag: gun
+  title: 何やってんだよ
+  texts:
+    - /td//ぐわっ！\bg{2}
+    - /rd//団長？何やってんだよ？団長！
+    - /org//ぐっ！うおぉ～～！
+    - /ass//うおっ！あっ！
+    - /org//はぁはぁはぁ……なんだよ、結構当たんじゃねぇか。ふっ……\bg{3}
+    - /rd//だ……団長……あっ……あぁ……
+    - /org//なんて声出してやがる……ライド。
+    - /rd//だって……だって……\bgm{0}
+    - /org//俺は鉄華団団長オルガ・イツカだぞ。こんくれぇなんてこたぁねぇ。
+    - /rd//そんな……俺なんかのために……
+    - /org//団員を守んのは俺の仕事だ。
+    - /rd//でも！
+    - /org//いいから行くぞ。皆が待ってんだ。それに……。
+    - ミカ、やっと分かったんだ。
+    - 俺たちにはたどりつく場所なんていらねぇ。ただ進み続けるだけでいい。止まんねぇかぎり、道は続く。
+    - /mktk//謝ったら許さない。
+    - /org//ああ分かってる。
+  next: end
+- tag: end
+  title: 止まるんじゃねぇぞ
+  texts:
+    - /org//俺は止まんねぇからよ、お前らが止まんねぇかぎり、その先に俺はいるぞ！\bgm{0}\bg{4}
+    - /org//だからよ、止まるんじゃねぇぞ……
diff --git a/examples/Orga/paras/zh-Hans/main.yaml b/examples/Orga/paras/zh-Hans/main.yaml
new file mode 100644
index 00000000..7fd62bf9
--- /dev/null
+++ b/examples/Orga/paras/zh-Hans/main.yaml
@@ -0,0 +1,48 @@
+- tag: main
+  title: 备车
+  texts:
+    - |
+      团长！车已经准备好了！
+      \switch{哦！}
+      \switch{不要停下来啊！}
+      \switch{停下！}
+- tag: walk
+  title: 安静
+  texts:
+    - 哦！那么我走了。
+    - 好，路上小心。
+    - |
+      感觉真安静啊，街上也没有加拉尔霍恩的人，和总部差别真大啊。
+      \switch{安全}
+      \switch{小心}
+    - 啊，可能是他们把火星的战力都调到那里去了。
+    - 不过这也和我们没关系了。
+    - 心情很愉快啊。
+    - /rd/加把劲骑士/那是当然的了！大家也有救了，{}又那么努力，我也\ruby{要加油了}{是加把劲骑士}！
+    - 啊。
+    - 没错，我们至今为止所做的一切，并不是全部徒劳的。今后也是，只要我们不停下脚步，道路就会不断延伸。
+- tag: gun
+  title: 你在干什么啊
+  texts:
+    - 咕哇！
+    - 团长，你在干什么啊，团长！
+    - 呃呃啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊！！！
+    - 呜哦！啊！
+    - 哈哈哈……什么嘛，我的枪法还挺准的嘛。呵……
+    - 团……团长……啊……啊……
+    - 你的声音为什么要颤抖，莱德。
+    - 因为……因为……
+    - 我可是铁华团团长奥尔加·伊兹卡啊，这点小伤无关紧要。
+    - 为什么……要为了保护我……
+    - 保护团员就是我的使命。
+    - 但是！
+    - 好了，快走吧！大家都在等着你们。而且……
+    - 三日，我终于明白了。
+    - 我们的目的地根本不重要，只要继续前进就好了，只要不停下脚步，道路就会不断延伸。
+    - 道歉的话我是不会原谅你的。
+    - 啊，我明白。
+- tag: end
+  title: 不要停下来啊
+  texts:
+    - 我是不会停下来的，只要你们不停下来，那前面就一定有我！
+    - 所以，不要停下来啊……
diff --git a/examples/Orga/res/ja.yaml b/examples/Orga/res/ja.yaml
new file mode 100644
index 00000000..77f38f08
--- /dev/null
+++ b/examples/Orga/res/ja.yaml
@@ -0,0 +1,7 @@
+ch_rd: ライド
+ch_org: オルガ
+ch_kdr: クーデリア
+ch_tkk: タカキ
+ch_td: チャド
+ch_ass: 暗殺者
+ch_mktk: 三日月
diff --git a/examples/Orga/res/zh-Hans.yaml b/examples/Orga/res/zh-Hans.yaml
new file mode 100644
index 00000000..aa247fdc
--- /dev/null
+++ b/examples/Orga/res/zh-Hans.yaml
@@ -0,0 +1,6 @@
+ch_rd: 莱德
+ch_org: 奥尔加
+ch_kdr: 古荻莉亚
+ch_tkk: 塔卡基
+ch_td: 查德
+ch_ass: 刺客
diff --git a/examples/Styles/config.yaml b/examples/Styles/config.yaml
index 12b184af..8aa42bcf 100644
--- a/examples/Styles/config.yaml
+++ b/examples/Styles/config.yaml
@@ -6,24 +6,5 @@ plugins:
     - basictex
     - markdown
 base_lang: en
-paras:
-  en:
-    - tag: md
-      texts:
-        - Markdown examples
-        - '# HEAD'
-        - Normal
-        - '*Italic*'
-        - '**Bold**'
-        - '`code`'
-        - Mixed normal *italic* **bold** `code`.
-      next: tex
-    - tag: tex
-      texts:
-        - LaTeX examples
-        - This is the first line.\par This is the second line.
-        - \textrm{Roman}
-        - \textsf{Sans-serif}
-        - \texttt{Monospace}
-        - Mixed \textrm{Roman} \textsf{Sans-serif} \texttt{Monospace}
-        - \ruby{Ruby}{ruby}
\ No newline at end of file
+paras: paras
+start: md
diff --git a/examples/Styles/paras/en/md.yaml b/examples/Styles/paras/en/md.yaml
new file mode 100644
index 00000000..9f0766c5
--- /dev/null
+++ b/examples/Styles/paras/en/md.yaml
@@ -0,0 +1,10 @@
+- tag: md
+  texts:
+    - Markdown examples
+    - '# HEAD'
+    - Normal
+    - '*Italic*'
+    - '**Bold**'
+    - '`code`'
+    - Mixed normal *italic* **bold** `code`.
+  next: tex
diff --git a/examples/Styles/paras/en/tex.yaml b/examples/Styles/paras/en/tex.yaml
new file mode 100644
index 00000000..4f1a2cc8
--- /dev/null
+++ b/examples/Styles/paras/en/tex.yaml
@@ -0,0 +1,9 @@
+- tag: tex
+  texts:
+    - LaTeX examples
+    - This is the first line.\par This is the second line.
+    - \textrm{Roman}
+    - \textsf{Sans-serif}
+    - \texttt{Monospace}
+    - Mixed \textrm{Roman} \textsf{Sans-serif} \texttt{Monospace}
+    - \ruby{Ruby}{ruby}
diff --git a/utils/ayaka-bindings-types/src/lib.rs b/utils/ayaka-bindings-types/src/lib.rs
index f0eaa68d..7689e4f5 100644
--- a/utils/ayaka-bindings-types/src/lib.rs
+++ b/utils/ayaka-bindings-types/src/lib.rs
@@ -236,6 +236,8 @@ pub type VarMap = HashMap<String, RawValue>;
 /// The serializable context.
 #[derive(Debug, Default, Clone, Deserialize, Serialize)]
 pub struct RawContext {
+    /// Current base paragraph tag.
+    pub cur_base_para: String,
     /// Current paragraph tag.
     pub cur_para: String,
     /// Current text index.
diff --git a/utils/ayaka-runtime/Cargo.toml b/utils/ayaka-runtime/Cargo.toml
index e26394de..be9710d5 100644
--- a/utils/ayaka-runtime/Cargo.toml
+++ b/utils/ayaka-runtime/Cargo.toml
@@ -20,10 +20,10 @@ wasmer = "2.3"
 wasmer-wasi = "2.3"
 unicode-width = "0.1"
 rt-format = "0.3"
-tokio = { version = "1", features = ["fs", "macros"] }
-tokio-stream = { version = "0.1", features = ["fs"] }
+tokio = { version = "1", features = ["macros", "sync"] }
 stream-future = "0.3"
 futures-util = "0.3"
+tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" }
 dirs = "4.0"
 scopeguard = "1.1"
 
diff --git a/utils/ayaka-runtime/src/config.rs b/utils/ayaka-runtime/src/config.rs
index 7603e246..629fc6a4 100644
--- a/utils/ayaka-runtime/src/config.rs
+++ b/utils/ayaka-runtime/src/config.rs
@@ -7,7 +7,7 @@ use crate::*;
 use serde::Deserialize;
 use std::{collections::HashMap, path::PathBuf};
 
-/// The paragraph in a game config.
+/// The paragraph in a paragraph config.
 #[derive(Debug, Deserialize)]
 pub struct Paragraph {
     /// The tag and key of a paragraph.
@@ -24,26 +24,27 @@ pub struct Paragraph {
     pub next: Option<String>,
 }
 
-/// The ayaka-game config.
+/// The Ayaka config.
 /// It should be deserialized from a YAML file.
 #[derive(Debug, Default, Deserialize)]
-pub struct Game {
+pub struct GameConfig {
     /// The title of the game.
     pub title: String,
     /// The author of the game.
     #[serde(default)]
     pub author: String,
-    /// The paragraphs, indexed by locale.
-    pub paras: HashMap<Locale, Vec<Paragraph>>,
+    /// The paragraphs path.
+    pub paras: PathBuf,
+    /// The start paragraph tag.
+    pub start: String,
     /// The plugin config.
     #[serde(default)]
     pub plugins: PluginConfig,
     /// The global game properties.
     #[serde(default)]
     pub props: HashMap<String, String>,
-    /// The resources, indexed by locale.
-    #[serde(default)]
-    pub res: HashMap<Locale, VarMap>,
+    /// The resources path.
+    pub res: Option<PathBuf>,
     /// The base language.
     /// If the runtime fails to choose a best match,
     /// it fallbacks to this one.
@@ -60,16 +61,31 @@ pub struct PluginConfig {
     pub modules: Vec<String>,
 }
 
+/// The full Ayaka game.
+/// It consists of global config and all paragraphs.
+pub struct Game {
+    /// The game config.
+    pub config: GameConfig,
+    /// The paragraphs, indexed by locale.
+    /// The inner is the paragraphs indexed by file names.
+    pub paras: HashMap<Locale, HashMap<String, Vec<Paragraph>>>,
+    /// The resources, indexed by locale.
+    pub res: HashMap<Locale, VarMap>,
+}
+
 impl Game {
     fn choose_from_keys<'a, V>(&'a self, loc: &Locale, map: &'a HashMap<Locale, V>) -> &'a Locale {
-        loc.choose_from(map.keys()).unwrap_or(&self.base_lang)
+        loc.choose_from(map.keys())
+            .unwrap_or(&self.config.base_lang)
     }
 
-    fn find_para(&self, loc: &Locale, tag: &str) -> Option<&Paragraph> {
+    fn find_para(&self, loc: &Locale, base_tag: &str, tag: &str) -> Option<&Paragraph> {
         if let Some(paras) = self.paras.get(loc) {
-            for p in paras {
-                if p.tag == tag {
-                    return Some(p);
+            if let Some(paras) = paras.get(base_tag) {
+                for p in paras.iter() {
+                    if p.tag == tag {
+                        return Some(p);
+                    }
                 }
             }
         }
@@ -77,16 +93,21 @@ impl Game {
     }
 
     /// Find a paragraph by tag, with specified locale.
-    pub fn find_para_fallback(&self, loc: &Locale, tag: &str) -> Fallback<&Paragraph> {
+    pub fn find_para_fallback(
+        &self,
+        loc: &Locale,
+        base_tag: &str,
+        tag: &str,
+    ) -> Fallback<&Paragraph> {
         let key = self.choose_from_keys(loc, &self.paras);
-        let base_key = self.choose_from_keys(&self.base_lang, &self.paras);
+        let base_key = self.choose_from_keys(&self.config.base_lang, &self.paras);
         Fallback::new(
             if key == base_key {
                 None
             } else {
-                self.find_para(key, tag)
+                self.find_para(key, base_tag, tag)
             },
-            self.find_para(base_key, tag),
+            self.find_para(base_key, base_tag, tag),
         )
     }
 
@@ -97,7 +118,7 @@ impl Game {
     /// Find the resource map with specified locale.
     pub fn find_res_fallback(&self, loc: &Locale) -> Fallback<&VarMap> {
         let key = self.choose_from_keys(loc, &self.res);
-        let base_key = self.choose_from_keys(&self.base_lang, &self.res);
+        let base_key = self.choose_from_keys(&self.config.base_lang, &self.res);
         Fallback::new(
             if key == base_key {
                 None
diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs
index 8592b78a..5a2a6804 100644
--- a/utils/ayaka-runtime/src/context.rs
+++ b/utils/ayaka-runtime/src/context.rs
@@ -44,6 +44,12 @@ pub enum OpenStatus {
     CreateRuntime,
     /// Loading the plugin.
     LoadPlugin(String, usize, usize),
+    /// Executing game plugins.
+    GamePlugin,
+    /// Loading the resources.
+    LoadResource,
+    /// Loading the paragraphs.
+    LoadParagraph,
 }
 
 impl Context {
@@ -51,15 +57,15 @@ impl Context {
     #[stream(OpenStatus, lifetime = "'a")]
     pub async fn open<'a>(path: impl AsRef<Path> + 'a, frontend: FrontendType) -> Result<Self> {
         yield OpenStatus::LoadProfile;
-        let file = tokio::fs::read(&path).await?;
-        let mut game: Game = serde_yaml::from_slice(&file)?;
+        let file = std::fs::read(&path)?;
+        let mut config: GameConfig = serde_yaml::from_slice(&file)?;
         let root_path = path
             .as_ref()
             .parent()
             .ok_or_else(|| anyhow!("Cannot get parent from input path."))?;
         let root_path = std::path::absolute(root_path)?;
         let runtime = {
-            let runtime = Runtime::load(&game.plugins.dir, &root_path, &game.plugins.modules);
+            let runtime = Runtime::load(&config.plugins.dir, &root_path, &config.plugins.modules);
             pin_mut!(runtime);
             while let Some(load_status) = runtime.next().await {
                 match load_status {
@@ -71,21 +77,68 @@ impl Context {
             }
             runtime.await?
         };
+
+        yield OpenStatus::GamePlugin;
         for m in &runtime.game_modules {
             let module = &runtime.modules[m];
             let ctx = GameProcessContextRef {
-                title: &game.title,
-                author: &game.author,
+                title: &config.title,
+                author: &config.author,
                 root_path: &root_path,
-                props: &game.props,
+                props: &config.props,
             };
             let res = module.process_game(ctx)?;
             for (key, value) in res.props {
-                game.props.insert(key, value);
+                config.props.insert(key, value);
+            }
+        }
+
+        yield OpenStatus::LoadResource;
+        let mut res = HashMap::new();
+        if let Some(res_path) = &config.res {
+            let res_path = root_path.join(res_path);
+            for rl in std::fs::read_dir(res_path)? {
+                let p = rl?.path();
+                if p.is_file() && p.extension().map(|ex| ex == "yaml").unwrap_or_default() {
+                    let loc = p
+                        .file_stem()
+                        .and_then(|s| s.to_string_lossy().parse::<Locale>().ok())
+                        .unwrap_or_default();
+                    let r = std::fs::read(p)?;
+                    let r = serde_yaml::from_slice(&r)?;
+                    res.insert(loc, r);
+                }
+            }
+        }
+
+        yield OpenStatus::LoadParagraph;
+        let mut paras = HashMap::new();
+        let paras_path = root_path.join(&config.paras);
+        for pl in std::fs::read_dir(paras_path)? {
+            let p = pl?.path();
+            if p.is_dir() {
+                let loc = p
+                    .file_name()
+                    .and_then(|s| s.to_string_lossy().parse::<Locale>().ok())
+                    .unwrap_or_default();
+                let mut paras_map = HashMap::new();
+                for p in std::fs::read_dir(p)? {
+                    let p = p?.path();
+                    if p.is_file() && p.extension().map(|ex| ex == "yaml").unwrap_or_default() {
+                        let key = p
+                            .file_stem()
+                            .map(|s| s.to_string_lossy().into_owned())
+                            .unwrap_or_default();
+                        let para = std::fs::read(p)?;
+                        let para = serde_yaml::from_slice(&para)?;
+                        paras_map.insert(key, para);
+                    }
+                }
+                paras.insert(loc, paras_map);
             }
         }
         Ok(Self {
-            game,
+            game: Game { config, paras, res },
             frontend,
             root_path,
             runtime,
@@ -104,6 +157,7 @@ impl Context {
     /// Initialize the [`ActionRecord`] with given record.
     pub fn init_context(&mut self, record: ActionRecord) {
         self.ctx = record.last_ctx_with_game(&self.game);
+        log::debug!("Context: {:?}", self.ctx);
         self.record = record;
         if !self.record.history.is_empty() {
             // If the record is not empty,
@@ -122,7 +176,7 @@ impl Context {
 
     fn current_paragraph(&self) -> Fallback<&Paragraph> {
         self.game
-            .find_para_fallback(self.locale(), &self.ctx.cur_para)
+            .find_para_fallback(self.locale(), &self.ctx.cur_base_para, &self.ctx.cur_para)
     }
 
     fn current_text(&self) -> Fallback<&String> {
@@ -199,9 +253,9 @@ impl Context {
 
         let para_name = self
             .current_paragraph()
-            .and_then(|p| p.title.as_ref())
-            .map(|s| s.escape_default().to_string())
-            .unwrap_or_default();
+            .and_then(|p| p.title.as_deref())
+            .unwrap_or_default()
+            .escape_default();
         let act_num = self.ctx.cur_act + 1;
         let show_code = &text[pre..post];
         let pre_code = &text[pre..loc.0];
@@ -261,7 +315,7 @@ impl Context {
                         if let Some(m) = self.runtime.text_modules.get(&name) {
                             let game_context = TextProcessContextRef {
                                 root_path: &self.root_path,
-                                game_props: &self.game.props,
+                                game_props: &self.game.config.props,
                                 frontend: self.frontend,
                             };
                             let mut res = self.runtime.modules.get(m).unwrap().dispatch_command(
@@ -363,7 +417,7 @@ impl Context {
             let module = &self.runtime.modules[action_module];
             let ctx = ActionProcessContextRef {
                 root_path: &self.root_path,
-                game_props: &self.game.props,
+                game_props: &self.game.config.props,
                 frontend: self.frontend,
                 last_action,
                 action: &action,
@@ -418,38 +472,48 @@ impl Context {
                 .and_modify(|act| *act = (*act).max(action.ctx.cur_act))
                 .or_insert(action.ctx.cur_act);
         }
-        let cur_para = self.current_paragraph();
-        if cur_para.is_some() {
+        let (cur_para, cur_text) = loop {
+            let cur_para = self.current_paragraph();
             let cur_text = self.current_text();
-            if cur_text.is_some() {
-                let text = cur_text.map(|act| self.parse_text_rich_error(act));
-                let para_title = cur_para.and_then(|p| p.title.as_ref()).cloned();
-                let actions = text.map(|t| {
-                    self.exact_text(para_title.clone(), t).unwrap_or_else(|e| {
-                        error!("Exact text error: {}", e);
-                        Action::default()
-                    })
-                });
-                let res = self.merge_action(actions).map(|act| {
-                    self.process_action(act).unwrap_or_else(|e| {
-                        error!("Error when processing action: {}", e);
-                        Action::default()
-                    })
-                });
-                self.ctx.cur_act += 1;
-                res
-            } else {
-                self.ctx.cur_para = cur_para
-                    .and_then(|p| p.next.as_ref())
-                    .map(|next| self.parse_text_rich_error(next))
-                    .map(|text| self.call(&text).into_str())
-                    .unwrap_or_default();
-                self.ctx.cur_act = 0;
-                self.next_run()
+            match (cur_para.is_some(), cur_text.is_some()) {
+                (true, true) => break (cur_para, cur_text),
+                (true, false) => {
+                    self.ctx.cur_para = cur_para
+                        .and_then(|p| p.next.as_ref())
+                        .map(|next| self.parse_text_rich_error(next))
+                        .map(|text| self.call(&text).into_str())
+                        .unwrap_or_default();
+                    self.ctx.cur_act = 0;
+                }
+                (false, _) => {
+                    if self.ctx.cur_base_para == self.ctx.cur_para {
+                        error!(
+                            "Cannot find paragraph \"{}\"",
+                            self.ctx.cur_para.escape_default()
+                        );
+                        return None;
+                    } else {
+                        self.ctx.cur_base_para = self.ctx.cur_para.clone();
+                    }
+                }
             }
-        } else {
-            None
-        }
+        };
+        let text = cur_text.map(|act| self.parse_text_rich_error(act));
+        let para_title = cur_para.and_then(|p| p.title.as_ref()).cloned();
+        let actions = text.map(|t| {
+            self.exact_text(para_title.clone(), t).unwrap_or_else(|e| {
+                error!("Exact text error: {}", e);
+                Action::default()
+            })
+        });
+        let res = self.merge_action(actions).map(|act| {
+            self.process_action(act).unwrap_or_else(|e| {
+                error!("Error when processing action: {}", e);
+                Action::default()
+            })
+        });
+        self.ctx.cur_act += 1;
+        res
     }
 
     /// Step back to the last run.
@@ -473,14 +537,17 @@ impl Context {
     pub fn check(&mut self) -> bool {
         let mut succeed = true;
         for paras in self.game.paras.values() {
-            for para in paras {
-                self.ctx.cur_para = para.tag.clone();
-                for (index, act) in para.texts.iter().enumerate() {
-                    self.ctx.cur_act = index;
-                    succeed &= self.check_text_rich_error(act);
-                }
-                if let Some(next) = &para.next {
-                    succeed &= self.check_text_rich_error(next);
+            for (base_tag, paras) in paras {
+                self.ctx.cur_base_para = base_tag.clone();
+                for para in paras.iter() {
+                    self.ctx.cur_para = para.tag.clone();
+                    for (index, act) in para.texts.iter().enumerate() {
+                        self.ctx.cur_act = index;
+                        succeed &= self.check_text_rich_error(act);
+                    }
+                    if let Some(next) = &para.next {
+                        succeed &= self.check_text_rich_error(next);
+                    }
                 }
             }
         }
diff --git a/utils/ayaka-runtime/src/plugin.rs b/utils/ayaka-runtime/src/plugin.rs
index 0bbf5599..71994ab4 100644
--- a/utils/ayaka-runtime/src/plugin.rs
+++ b/utils/ayaka-runtime/src/plugin.rs
@@ -6,13 +6,12 @@
 use crate::*;
 use anyhow::Result;
 use ayaka_bindings_types::*;
-use futures_util::TryStreamExt;
 use log::warn;
 use scopeguard::defer;
 use serde::{de::DeserializeOwned, Serialize};
 use std::{collections::HashMap, path::Path};
 use stream_future::stream;
-use tokio_stream::wrappers::ReadDirStream;
+use tryiterator::TryIteratorExt;
 use wasmer::*;
 use wasmer_wasi::*;
 
@@ -197,14 +196,10 @@ impl Runtime {
         let mut text_modules = HashMap::new();
         let mut game_modules = vec![];
         let paths = if names.is_empty() {
-            ReadDirStream::new(tokio::fs::read_dir(path).await?)
-                .try_filter_map(|f| async move {
+            std::fs::read_dir(path)?
+                .try_filter_map(|f| {
                     let p = f.path();
-                    if p.extension()
-                        .map(|s| s.to_string_lossy())
-                        .unwrap_or_default()
-                        == "wasm"
-                    {
+                    if p.is_file() && p.extension().map(|s| s == "wasm").unwrap_or_default() {
                         let name = p
                             .file_stem()
                             .map(|s| s.to_string_lossy())
@@ -215,8 +210,7 @@ impl Runtime {
                         Ok(None)
                     }
                 })
-                .try_collect::<Vec<_>>()
-                .await?
+                .try_collect::<Vec<_>>()?
         } else {
             names
                 .iter()
@@ -234,7 +228,7 @@ impl Runtime {
         let total_len = paths.len();
         for (i, (name, p)) in paths.into_iter().enumerate() {
             yield LoadStatus::LoadPlugin(name.clone(), i, total_len);
-            let buf = tokio::fs::read(p).await?;
+            let buf = std::fs::read(p)?;
             let module = Module::from_binary(&store, &buf)?;
             let runtime = Host::new(&module, &import_object)?;
             let plugin_type = runtime.plugin_type()?;
diff --git a/utils/ayaka-runtime/src/settings.rs b/utils/ayaka-runtime/src/settings.rs
index 89e2af7f..d4869cb5 100644
--- a/utils/ayaka-runtime/src/settings.rs
+++ b/utils/ayaka-runtime/src/settings.rs
@@ -4,13 +4,12 @@ pub use ayaka_bindings_types::VarMap;
 use crate::*;
 use anyhow::{anyhow, Result};
 use dirs::{config_dir, data_local_dir};
-use futures_util::TryStreamExt;
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use std::{
     collections::HashMap,
     path::{Path, PathBuf},
 };
-use tokio_stream::wrappers::ReadDirStream;
+use tryiterator::TryIteratorExt;
 
 /// The settings of the game.
 #[derive(Debug, Default, Clone, Deserialize, Serialize)]
@@ -53,35 +52,29 @@ impl ActionRecord {
     /// and if the history is empty, create a new [`RawContext`] from the game.
     pub fn last_ctx_with_game(&self, game: &Game) -> RawContext {
         self.last_ctx().cloned().unwrap_or_else(|| RawContext {
-            cur_para: game
-                .paras
-                .get(&game.base_lang)
-                .and_then(|paras| paras.first().map(|p| p.tag.clone()))
-                .unwrap_or_else(|| {
-                    log::warn!("There is no paragraph in the game.");
-                    Default::default()
-                }),
+            cur_base_para: game.config.start.clone(),
+            cur_para: game.config.start.clone(),
             ..Default::default()
         })
     }
 }
 
-async fn load_file<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
-    let buffer = tokio::fs::read(path).await?;
+fn load_file<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
+    let buffer = std::fs::read(path)?;
     Ok(serde_json::from_slice(&buffer)?)
 }
 
-async fn save_file<T: Serialize>(data: &T, path: impl AsRef<Path>, pretty: bool) -> Result<()> {
+fn save_file<T: Serialize>(data: &T, path: impl AsRef<Path>, pretty: bool) -> Result<()> {
     let path = path.as_ref();
     if let Some(parent) = path.parent() {
-        tokio::fs::create_dir_all(parent).await?;
+        std::fs::create_dir_all(parent)?;
     }
     let buffer = if pretty {
         serde_json::to_vec_pretty(data)
     } else {
         serde_json::to_vec(data)
     }?;
-    tokio::fs::write(path, &buffer).await?;
+    std::fs::write(path, &buffer)?;
     Ok(())
 }
 
@@ -91,13 +84,13 @@ fn settings_path(ident: &str) -> Result<PathBuf> {
 }
 
 /// Load settings from JSON file.
-pub async fn load_settings(ident: &str) -> Result<Settings> {
-    load_file(settings_path(ident)?).await
+pub fn load_settings(ident: &str) -> Result<Settings> {
+    load_file(settings_path(ident)?)
 }
 
 /// Save settings into pretty JSON file.
-pub async fn save_settings(ident: &str, data: &Settings) -> Result<()> {
-    save_file(data, settings_path(ident)?, true).await
+pub fn save_settings(ident: &str, data: &Settings) -> Result<()> {
+    save_file(data, settings_path(ident)?, true)
 }
 
 fn records_path(ident: &str, game: &str) -> Result<PathBuf> {
@@ -110,51 +103,45 @@ fn global_record_path(ident: &str, game: &str) -> Result<PathBuf> {
 }
 
 /// Load [`GlobalRecord`] from the records folder.
-pub async fn load_global_record(ident: &str, game: &str) -> Result<GlobalRecord> {
-    load_file(global_record_path(ident, game)?).await
+pub fn load_global_record(ident: &str, game: &str) -> Result<GlobalRecord> {
+    load_file(global_record_path(ident, game)?)
 }
 
 /// Save [`GlobalRecord`] into the records folder.
-pub async fn save_global_record(ident: &str, game: &str, data: &GlobalRecord) -> Result<()> {
-    save_file(data, global_record_path(ident, game)?, false).await
+pub fn save_global_record(ident: &str, game: &str, data: &GlobalRecord) -> Result<()> {
+    save_file(data, global_record_path(ident, game)?, false)
 }
 
 /// Load all [`ActionRecord`] from the records folder.
-pub async fn load_records(ident: &str, game: &str) -> Result<Vec<ActionRecord>> {
+pub fn load_records(ident: &str, game: &str) -> Result<Vec<ActionRecord>> {
     let ctx_path = records_path(ident, game)?;
-    let contexts = ReadDirStream::new(tokio::fs::read_dir(ctx_path).await?)
+    let contexts = std::fs::read_dir(ctx_path)?
         .map_err(anyhow::Error::from)
-        .try_filter_map(|entry| async move {
+        .try_filter_map(|entry| {
             let p = entry.path();
-            if p.extension()
-                .map(|s| s.to_string_lossy())
-                .unwrap_or_default()
-                == "json"
-                && p.file_stem()
-                    .map(|s| s.to_string_lossy())
+            if p.is_file()
+                && p.file_name()
+                    .map(|s| s == "global.json")
                     .unwrap_or_default()
-                    != "global"
             {
-                Ok(Some(load_file(&p).await?))
+                Ok(Some(load_file(&p)?))
             } else {
                 Ok(None)
             }
         })
-        .try_collect()
-        .await?;
+        .try_collect()?;
     Ok(contexts)
 }
 
 /// Save all [`ActionRecord`] into the records folder.
-pub async fn save_records(ident: &str, game: &str, contexts: &[ActionRecord]) -> Result<()> {
+pub fn save_records(ident: &str, game: &str, contexts: &[ActionRecord]) -> Result<()> {
     let ctx_path = records_path(ident, game)?;
     for (i, ctx) in contexts.iter().enumerate() {
         save_file(
             ctx,
             ctx_path.join(i.to_string()).with_extension("json"),
             false,
-        )
-        .await?;
+        )?;
     }
     Ok(())
 }


Test patch: