galbook 0.0.0-stage → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.agents/skills/galbook/SKILL.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +114 -0
  4. package/README.md +133 -3
  5. package/bin/galbook.js +53 -0
  6. package/cordis.patch.yml +30 -0
  7. package/dsh-plugin/README.md +49 -0
  8. package/dsh-plugin/install.py +190 -0
  9. package/galbook/__init__.py +15 -0
  10. package/galbook/__main__.py +138 -0
  11. package/galbook/build.py +89 -0
  12. package/galbook/cli.py +29 -0
  13. package/galbook/demo/fog-harbor/assets/cg_dawn_together.svg +1 -0
  14. package/galbook/demo/fog-harbor/assets/cg_old_letter.svg +1 -0
  15. package/galbook/demo/fog-harbor/assets/cg_ship.svg +1 -0
  16. package/galbook/demo/fog-harbor/assets/harbor_night.svg +1 -0
  17. package/galbook/demo/fog-harbor/assets/lamp_room.svg +1 -0
  18. package/galbook/demo/fog-harbor/assets/lamp_room_dawn.svg +1 -0
  19. package/galbook/demo/fog-harbor/assets/path_tide.svg +1 -0
  20. package/galbook/demo/fog-harbor/assets/shio_normal.svg +1 -0
  21. package/galbook/demo/fog-harbor/assets/shio_smile.svg +1 -0
  22. package/galbook/demo/fog-harbor/assets/shio_surprised.svg +1 -0
  23. package/galbook/demo/fog-harbor/assets/warehouse.svg +1 -0
  24. package/galbook/demo/fog-harbor/story.gal +97 -0
  25. package/galbook/demo/fog-harbor-en/assets/cg_dawn_together.svg +1 -0
  26. package/galbook/demo/fog-harbor-en/assets/cg_old_letter.svg +1 -0
  27. package/galbook/demo/fog-harbor-en/assets/cg_ship.svg +1 -0
  28. package/galbook/demo/fog-harbor-en/assets/harbor_night.svg +1 -0
  29. package/galbook/demo/fog-harbor-en/assets/lamp_room.svg +1 -0
  30. package/galbook/demo/fog-harbor-en/assets/lamp_room_dawn.svg +1 -0
  31. package/galbook/demo/fog-harbor-en/assets/path_tide.svg +1 -0
  32. package/galbook/demo/fog-harbor-en/assets/shio_normal.svg +1 -0
  33. package/galbook/demo/fog-harbor-en/assets/shio_smile.svg +1 -0
  34. package/galbook/demo/fog-harbor-en/assets/shio_surprised.svg +1 -0
  35. package/galbook/demo/fog-harbor-en/assets/warehouse.svg +1 -0
  36. package/galbook/demo/fog-harbor-en/story.gal +97 -0
  37. package/galbook/demo_boot.py +63 -0
  38. package/galbook/engine.py +325 -0
  39. package/galbook/expr.py +107 -0
  40. package/galbook/i18n.py +85 -0
  41. package/galbook/importers/__init__.py +106 -0
  42. package/galbook/importers/common.py +94 -0
  43. package/galbook/importers/galscript.py +218 -0
  44. package/galbook/importers/kag.py +326 -0
  45. package/galbook/importers/legacy.py +122 -0
  46. package/galbook/importers/nscripter.py +314 -0
  47. package/galbook/importers/qlie.py +498 -0
  48. package/galbook/importers/renpy.py +333 -0
  49. package/galbook/importers/textdump.py +128 -0
  50. package/galbook/ir.py +266 -0
  51. package/galbook/mcp_server.py +102 -0
  52. package/galbook/play.py +374 -0
  53. package/galbook/server.py +199 -0
  54. package/galbook/static/play.html +151 -0
  55. package/galbook/store.py +160 -0
  56. package/galbook/tools.py +99 -0
  57. package/galbook/web.py +316 -0
  58. package/icon.svg +21 -0
  59. package/locale/en.json +6 -0
  60. package/locale/zh.json +6 -0
  61. package/package.json +58 -6
@@ -0,0 +1,122 @@
1
+ """老式书稿(book.json + chNNN.json:按卷分章、一页一句、没有原始选项数据)→ 可玩剧本。
2
+
3
+ 没有脚本里的真实分支,所以:有「共通」卷就先读共通,读完让 agent 选走哪条线(按卷);
4
+ 每条线读完算一个结局。mode = "linear",并在 warnings 里写清楚这是「按卷生成的选项」。"""
5
+ import json
6
+ import os
7
+ import threading
8
+
9
+ from ..ir import story_from_dict
10
+ from .common import is_h_title
11
+
12
+ _cache = {}
13
+ _lock = threading.Lock()
14
+
15
+
16
+ def _load(p):
17
+ with open(p, encoding="utf-8") as f:
18
+ return json.load(f)
19
+
20
+
21
+ def _sig(book_dir):
22
+ bj = os.path.join(book_dir, "book.json")
23
+ st = os.stat(bj)
24
+ return (st.st_mtime_ns, st.st_size)
25
+
26
+
27
+ def load(book_dir):
28
+ sig = _sig(book_dir)
29
+ with _lock:
30
+ hit = _cache.get(book_dir)
31
+ if hit and hit[0] == sig:
32
+ return hit[1]
33
+ meta, scenes = build(book_dir)
34
+ story = story_from_dict(meta, scenes)
35
+ story.mode = "linear"
36
+ with _lock:
37
+ _cache[book_dir] = (sig, story)
38
+ return story
39
+
40
+
41
+ def _sprites(step):
42
+ out = {}
43
+ for i, s in enumerate(step.get("sprites") or []):
44
+ if isinstance(s, str):
45
+ out["#%d" % i] = {"img": s, "pos": "center"}
46
+ elif isinstance(s, dict) and s.get("img"):
47
+ out[s.get("name") or "#%d" % i] = {"img": s["img"], "pos": s.get("pos") or "center"}
48
+ return out
49
+
50
+
51
+ def build(book_dir):
52
+ b = _load(os.path.join(book_dir, "book.json"))
53
+ vols = b.get("volumes")
54
+ if not vols:
55
+ vols = [{"title": "", "chapters": b.get("chapters", [])}]
56
+ scenes, smeta = {}, {}
57
+ vol_entries = []
58
+ for vi, v in enumerate(vols):
59
+ chs = v.get("chapters") or []
60
+ first = None
61
+ prev = None
62
+ for ci, c in enumerate(chs):
63
+ sid = "v%d_%s" % (vi, c.get("id") or ("c%d" % ci))
64
+ path = os.path.join(book_dir, c["file"])
65
+ try:
66
+ d = _load(path)
67
+ except (OSError, ValueError):
68
+ continue
69
+ title = d.get("title") or c.get("title") or sid
70
+ ops = [{"op": "label", "name": "top"}]
71
+ bg = cg = None
72
+ sp = {}
73
+ for st in d.get("steps") or []:
74
+ if st.get("bg") != bg:
75
+ bg = st.get("bg")
76
+ ops.append({"op": "bg", "img": bg})
77
+ cg = None
78
+ if st.get("cg") != cg:
79
+ cg = st.get("cg")
80
+ ops.append({"op": "cg", "img": cg})
81
+ ns = _sprites(st)
82
+ if ns != sp:
83
+ ops.append({"op": "sprites_clear"})
84
+ for name, x in ns.items():
85
+ ops.append({"op": "sprite", "name": name, "img": x["img"], "pos": x["pos"]})
86
+ sp = ns
87
+ who = st.get("speaker") or ""
88
+ if who == "旁白":
89
+ who = ""
90
+ ops.append({"op": "say", "who": who.split("@")[0], "text": st.get("text", "")})
91
+ scenes[sid] = {"id": sid, "title": title, "ops": ops}
92
+ smeta[sid] = {"title": title, "route": v.get("title", ""), "tags": ["h"] if is_h_title(title) else []}
93
+ if prev:
94
+ scenes[prev]["ops"].append({"op": "jump", "scene": sid})
95
+ first = first or sid
96
+ prev = sid
97
+ if first:
98
+ vol_entries.append({"title": v.get("title") or b.get("title", ""), "first": first, "last": prev})
99
+ common = [e for e in vol_entries if "共通" in e["title"] or "common" in e["title"].lower()]
100
+ others = [e for e in vol_entries if e not in common]
101
+ start_ops = [{"op": "label", "name": "top"}]
102
+ if len(vol_entries) == 1:
103
+ start_ops.append({"op": "jump", "scene": vol_entries[0]["first"]})
104
+ scenes[vol_entries[0]["last"]]["ops"].append({"op": "end", "kind": "ending", "name": vol_entries[0]["title"]})
105
+ else:
106
+ chooser = {"op": "choice", "id": "route", "prompt": "选一条线读下去(老书稿没有原始选项,按卷生成)",
107
+ "options": [{"text": e["title"], "goto": {"scene": e["first"]}} for e in others]}
108
+ if common:
109
+ c = common[0]
110
+ start_ops.append({"op": "jump", "scene": c["first"]})
111
+ scenes[c["last"]]["ops"].append(chooser)
112
+ else:
113
+ start_ops.append(chooser)
114
+ for e in others:
115
+ scenes[e["last"]]["ops"].append({"op": "end", "kind": "ending", "name": e["title"]})
116
+ scenes["__start__"] = {"id": "__start__", "title": b.get("title", ""), "ops": start_ops}
117
+ smeta["__start__"] = {"title": b.get("title", "")}
118
+ meta = {"format": "galbook/1", "title": b.get("title", ""), "engine": "book-legacy",
119
+ "importer": "galbook-legacy-1", "mode": "linear", "entry": {"scene": "__start__", "label": "top"},
120
+ "scenes": smeta, "routes": [{"id": e["first"], "title": e["title"]} for e in vol_entries],
121
+ "warnings": ["老书稿没有原始脚本里的选项与跳转;这里的选项是按卷(路线)生成的,不代表游戏原本的分支。"]}
122
+ return meta, scenes
@@ -0,0 +1,314 @@
1
+ """NScripter / ONScripter(0.txt…9.txt 或 nscript.dat)→ galbook/1。老游戏、同人作品很多是这个。
2
+
3
+ nscript.dat 每字节 XOR 0x84 就是文本。认的写法:
4
+ *label ;注释 一行多条用 : 分隔
5
+ 文字行(不以英文指令开头);行尾 \\ 换页、@ 等点击;【名字】「…」 或 名字「…」 认说话人
6
+ select "文字",*l1,"文字",*l2 …(可跨行) selgosub 同上但是调用 selnum %v,"a","b"(把序号写进变量)
7
+ goto *l gosub *l return jumpf/jumpb(~ 标记)不支持会报
8
+ mov %0,1 add/sub/mul/div %0,1 inc/dec %0 if 条件 指令 / notif 条件 指令
9
+ bg "x.bmp",1 / bg black,1 ld l|c|r,":a;x.bmp",1 cl l|c|r|a,1 bgm "x" / stop
10
+ end → 结局;reset / definereset → 回标题
11
+ %0-%199 是本局变量 f.v*;%200 起(globalon 的全局区)当系统变量 s.v*;$n 字符串 → f.str*
12
+ """
13
+ import os
14
+ import re
15
+
16
+ from .common import SceneB, Assets, is_h_title
17
+
18
+
19
+ def _readb(p):
20
+ with open(p, "rb") as f:
21
+ return f.read()
22
+
23
+ CMD = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\b\s*(.*)$")
24
+ GLOBAL_BORDER = 200
25
+
26
+
27
+ def load_script(src):
28
+ """返回脚本全文(str)和来源说明。"""
29
+ p = None
30
+ for name in ("nscript.dat", "nscript.___"):
31
+ for dp, _, fns in os.walk(src):
32
+ for fn in fns:
33
+ if fn.lower() == name:
34
+ p = os.path.join(dp, fn)
35
+ break
36
+ if p:
37
+ with open(p, "rb") as fh:
38
+ raw = bytes(b ^ 0x84 for b in fh.read())
39
+ return _dec(raw), os.path.basename(p)
40
+ parts = []
41
+ for dp, _, fns in os.walk(src):
42
+ for fn in sorted(fns):
43
+ if re.match(r"^\d{1,2}\.txt$", fn.lower()):
44
+ parts.append((int(fn.split(".")[0]), os.path.join(dp, fn)))
45
+ if not parts:
46
+ raise ValueError("没找到 nscript.dat 或 0.txt")
47
+ return "\n".join(_dec(_readb(p)) for _, p in sorted(parts)), "%d 个 N.txt" % len(parts)
48
+
49
+
50
+ def _dec(raw):
51
+ for e in ("utf-8-sig", "cp932", "gbk"):
52
+ try:
53
+ return raw.decode(e)
54
+ except UnicodeDecodeError:
55
+ pass
56
+ return raw.decode("cp932", "replace")
57
+
58
+
59
+ def var(tok):
60
+ tok = tok.strip()
61
+ m = re.match(r"^%(\d+)$", tok)
62
+ if m:
63
+ n = int(m.group(1))
64
+ return ("s.v%d" if n >= GLOBAL_BORDER else "f.v%d") % n
65
+ m = re.match(r"^\$(\d+)$", tok)
66
+ if m:
67
+ return "f.str%s" % m.group(1)
68
+ return None
69
+
70
+
71
+ def conv_expr(e):
72
+ e = re.sub(r"%(\d+)", lambda m: var("%" + m.group(1)), e)
73
+ e = re.sub(r"\$(\d+)", lambda m: var("$" + m.group(1)), e)
74
+ e = re.sub(r"(?<![=!<>])=(?!=)", "==", e)
75
+ e = e.replace("<>", "!=")
76
+ e = re.sub(r"(?<!&)&(?!&)", "&&", e)
77
+ return e
78
+
79
+
80
+ def _split_colon(line):
81
+ out, cur, q = [], [], False
82
+ for ch in line:
83
+ if ch == '"':
84
+ q = not q
85
+ if ch == ":" and not q:
86
+ out.append("".join(cur))
87
+ cur = []
88
+ elif ch == ";" and not q:
89
+ break
90
+ else:
91
+ cur.append(ch)
92
+ out.append("".join(cur))
93
+ return [x.strip() for x in out if x.strip()]
94
+
95
+
96
+ def _args(s):
97
+ out, cur, q = [], [], False
98
+ for ch in s:
99
+ if ch == '"':
100
+ q = not q
101
+ cur.append(ch)
102
+ elif ch == "," and not q:
103
+ out.append("".join(cur).strip())
104
+ cur = []
105
+ else:
106
+ cur.append(ch)
107
+ if cur or out:
108
+ out.append("".join(cur).strip())
109
+ return out
110
+
111
+
112
+ def _unq(s):
113
+ s = s.strip()
114
+ return s[1:-1] if len(s) >= 2 and s[0] == '"' and s[-1] == '"' else s
115
+
116
+
117
+ class Importer(object):
118
+ def __init__(self, src, assets=None, entry="start"):
119
+ self.src, self.assets, self.entry = src, assets or Assets(), entry
120
+ self.warnings, self.unknown = [], {}
121
+ self.stats = {"files": 1, "says": 0, "choices": 0, "options": 0, "jumps": 0, "calls": 0, "sets": 0, "ifs": 0}
122
+ self.system_vars = set()
123
+ self.buf = []
124
+
125
+ def build(self):
126
+ text, origin = load_script(self.src)
127
+ self.stats["origin"] = origin
128
+ b = SceneB("script", "script")
129
+ lines = text.splitlines()
130
+ i = 0
131
+ in_define = False
132
+ while i < len(lines):
133
+ s = lines[i].strip()
134
+ i += 1
135
+ if not s or s.startswith(";"):
136
+ continue
137
+ if s.startswith("*"):
138
+ self._flush(b)
139
+ name = s[1:].split()[0].split(":")[0]
140
+ in_define = name.lower() == "define"
141
+ b.add("label", name=name.lower())
142
+ continue
143
+ if in_define:
144
+ continue
145
+ # select 跨行:行尾逗号就接下一行
146
+ while re.match(r"^(select|selgosub|selnum|csel)\b", s, re.I) and s.rstrip().endswith(",") and i < len(lines):
147
+ s = s.rstrip() + lines[i].strip()
148
+ i += 1
149
+ if CMD.match(s) and not re.match(r"^[A-Za-z]+[「『]", s):
150
+ for part in _split_colon(s):
151
+ self._cmd(b, part)
152
+ else:
153
+ self._text(b, s)
154
+ self._flush(b)
155
+ start = SceneB("__start__", "开始", tags=["system"])
156
+ start.add("label", name="top")
157
+ labels = {op["name"] for op in b.ops if op["op"] == "label"}
158
+ ent = self.entry if self.entry in labels else ("start" if "start" in labels else None)
159
+ if not ent:
160
+ raise ValueError("脚本里没有 *start")
161
+ start.add("jump", scene="script", label=ent)
162
+ scenes = {"__start__": start.data(), "script": b.data()}
163
+ meta = {"format": "galbook/1", "title": "", "engine": "nscripter", "importer": "galbook-nscripter-1",
164
+ "entry": {"scene": "__start__", "label": "top"},
165
+ "scenes": {k: {"title": v["title"], "tags": v["tags"]} for k, v in scenes.items()},
166
+ "routes": [], "system_vars": sorted(self.system_vars),
167
+ "stats": dict(self.stats, unknown=sorted(self.unknown.items(), key=lambda x: -x[1])[:20],
168
+ assets=self.assets.report()),
169
+ "warnings": self.warnings[:200]}
170
+ return meta, scenes
171
+
172
+ def _text(self, b, s):
173
+ page = s.endswith("\\") or s.endswith("@")
174
+ t = s.rstrip("\\@").replace("@", "")
175
+ t = re.sub(r"#[0-9A-Fa-f]{6}", "", t)
176
+ self.buf.append(t)
177
+ if page or True:
178
+ self._flush(b)
179
+
180
+ def _flush(self, b):
181
+ t = "".join(self.buf).strip()
182
+ self.buf = []
183
+ if not t:
184
+ return
185
+ who = ""
186
+ m = re.match(r"^【([^】]{1,16})】\s*(.*)$", t) or re.match(r"^([^「『\s]{1,8})([「『].*)$", t)
187
+ if m:
188
+ who, t = m.group(1), m.group(2)
189
+ b.add("say", who=who, text=t)
190
+ self.stats["says"] += 1
191
+
192
+ def _jump_label(self, tok):
193
+ tok = tok.strip()
194
+ return tok[1:].lower() if tok.startswith("*") else None
195
+
196
+ def _cmd(self, b, s):
197
+ m = CMD.match(s)
198
+ if not m:
199
+ return self._text(b, s)
200
+ c, rest = m.group(1).lower(), m.group(2).strip()
201
+ a = _args(rest) if rest else []
202
+ if c in ("goto", "gosub"):
203
+ lb = self._jump_label(rest)
204
+ if lb:
205
+ self._flush(b)
206
+ b.add("jump" if c == "goto" else "call", label=lb)
207
+ self.stats["jumps" if c == "goto" else "calls"] += 1
208
+ elif c == "return":
209
+ self._flush(b)
210
+ b.add("ret")
211
+ elif c in ("select", "selgosub", "csel"):
212
+ self._flush(b)
213
+ opts = []
214
+ for k in range(0, len(a) - 1, 2):
215
+ lb = self._jump_label(a[k + 1])
216
+ if lb:
217
+ opts.append({"text": _unq(a[k]), "goto": {"label": lb}})
218
+ if c == "selgosub":
219
+ # 调用式:选完 gosub,回来接着往下
220
+ ret = "__selret%d" % len(b.ops)
221
+ for o in opts:
222
+ o["goto"] = {"label": "__selcall_%s_%d" % (o["goto"]["label"], len(b.ops))}
223
+ b.add("choice", id="c%d" % len(b.ops), options=opts)
224
+ b.add("jump", label=ret)
225
+ for o, k in zip(opts, range(0, len(a) - 1, 2)):
226
+ b.add("label", name=o["goto"]["label"])
227
+ b.add("call", label=self._jump_label(a[k + 1]))
228
+ b.add("jump", label=ret)
229
+ b.add("label", name=ret)
230
+ else:
231
+ b.add("choice", id="c%d" % len(b.ops), options=opts)
232
+ self.stats["choices"] += 1
233
+ self.stats["options"] += len(opts)
234
+ elif c == "selnum":
235
+ self._flush(b)
236
+ v = var(a[0]) if a else None
237
+ opts = [{"text": _unq(x), "set": "%s = %d" % (v, k)} for k, x in enumerate(a[1:])] if v else []
238
+ if opts:
239
+ b.add("choice", id="c%d" % len(b.ops), options=opts)
240
+ self.stats["choices"] += 1
241
+ self.stats["options"] += len(opts)
242
+ elif c in ("mov", "add", "sub", "mul", "div", "mod"):
243
+ v = var(a[0]) if a else None
244
+ if v and len(a) > 1:
245
+ rhs = conv_expr(a[1])
246
+ e = {"mov": "%s = %s", "add": "%s += %s", "sub": "%s -= %s", "mul": "%s *= %s"}.get(c)
247
+ e = (e % (v, rhs)) if e else ("%s = %s %s %s" % (v, v, "/" if c == "div" else "%", rhs))
248
+ b.add("set", expr=e)
249
+ self.stats["sets"] += 1
250
+ if v.startswith("s."):
251
+ self.system_vars.add(v[2:])
252
+ elif c in ("inc", "dec"):
253
+ v = var(rest)
254
+ if v:
255
+ b.add("set", expr="%s %s= 1" % (v, "+" if c == "inc" else "-"))
256
+ elif c in ("if", "notif"):
257
+ mm = re.match(r"^(.*?)\s+((?:goto|gosub|mov|add|sub|inc|dec|end|return|reset|select)\b.*)$", rest, re.I)
258
+ if not mm:
259
+ self.unknown["if"] = self.unknown.get("if", 0) + 1
260
+ return
261
+ cond = conv_expr(mm.group(1))
262
+ if c == "notif":
263
+ cond = "!(%s)" % cond
264
+ n0 = len(b.ops)
265
+ self._cmd(b, mm.group(2))
266
+ for op in b.ops[n0:]:
267
+ op["if"] = cond if "if" not in op else "(%s) && (%s)" % (cond, op["if"])
268
+ self.stats["ifs"] += 1
269
+ elif c == "bg":
270
+ self._flush(b)
271
+ nm = _unq(a[0]) if a else ""
272
+ b.add("sprites_clear")
273
+ b.add("bg", img=None if nm.lower() in ("black", "white", "") or nm.startswith("#") else (self.assets.find(nm) or nm))
274
+ elif c == "ld":
275
+ self._flush(b)
276
+ if len(a) >= 2:
277
+ f = _unq(a[1]).split(";")[-1]
278
+ b.add("sprite", name=a[0], img=self.assets.find(f) or f, pos={"l": "left", "c": "center", "r": "right"}.get(a[0], a[0]))
279
+ elif c == "cl":
280
+ if a and a[0] == "a":
281
+ b.add("sprites_clear")
282
+ elif a:
283
+ b.add("sprite", name=a[0], img=None)
284
+ elif c in ("bgm", "mp3loop", "mp3", "play"):
285
+ b.add("bgm", name=_unq(a[0]) if a else None)
286
+ elif c in ("stop", "mp3stop", "playstop"):
287
+ b.add("bgm", name=None)
288
+ elif c == "end":
289
+ self._flush(b)
290
+ b.add("end", kind="ending", how="end")
291
+ elif c in ("reset", "definereset"):
292
+ self._flush(b)
293
+ b.add("end", kind="title", how="reset")
294
+ elif c in ("jumpf", "jumpb", "tablegoto"):
295
+ self.warnings.append("%s 不支持(只能人工改成 goto): %s" % (c, s))
296
+ elif c in ("wait", "delay", "click", "lr_trap", "trap", "print", "effect", "dwave", "dwaveloop", "wave",
297
+ "waveloop", "wavestop", "btndef", "btn", "btnwait", "btnwait2", "spbtn", "lsp", "csp", "msp",
298
+ "amsp", "vsp", "texton", "textoff", "textclear", "erasetextwindow", "setwindow", "setwindow2",
299
+ "monocro", "nega", "quake", "quakex", "quakey", "locate", "br", "saveon", "saveoff", "rmode",
300
+ "skipoff", "clickvoice", "voicevol", "bgmvol", "sevol", "humanz", "windoweffect", "puttext",
301
+ "game", "caption", "versionstr", "globalon", "nsa", "ns2", "ns3", "numalias", "stralias",
302
+ "defaultspeed", "menuselectcolor", "menusetwindow", "savenumber", "rmenu", "kidokuskip",
303
+ "mode_saya", "effectblank", "maxkaisoupage", "underline", "transmode", "textspeed",
304
+ "setcursor", "abssetcursor", "linepage", "linepage2", "systemcall", "savegame", "loadgame",
305
+ "chvol", "selectcolor", "selectvoice", "automode", "filelog", "labellog", "kidokumode",
306
+ "shadedistance", "usewheel", "useescspc", "getcursorpos", "time", "date", "getparam", "itoa",
307
+ "atoi", "len", "mid", "cmp", "add_", "for", "next", "break", "dim", "movl", "rnd", "rnd2"):
308
+ pass
309
+ else:
310
+ self.unknown[c] = self.unknown.get(c, 0) + 1
311
+
312
+
313
+ def build(src, assets=None, entry="start", **_):
314
+ return Importer(src, assets, entry or "start").build()