プラグインの書き方
作品固有の命令、演出、システム画面は、プラグイン(npm のパッケージ)として足す。エンジン本体には手を入れない。
動く例は examples/stamp-plugin(画面にスタンプを押す @stamp と @stamp-clear)。
置き方
プロジェクトの liminalia.json の plugins に、パッケージ名かフォルダを書く。
{ "plugins": ["./plugins/stamp"] }
パッケージの package.json の exports に、次の入口を置く(どれも任意)。
| 入口 | default export | 読むもの |
|---|---|---|
./commands | CommandPlugin(@liminalia/sdk の defineCommandPlugin) | 検査、画面なしの実行、取り込み、書き出し、プレイヤー(Node でもブラウザでも読む) |
./web | WebPlugin(@liminalia/web の defineWebPlugin) | プレイヤーだけ(ブラウザ) |
./import | TyranoImportProfile(@liminalia/ks-import) | lim import、lim backport、lim reference、開発サーバーの立ち絵の位置調整画面(writeCast)だけ(Node) |
./studio | StudioPlugin(@liminalia/studio/plugin の defineStudioPlugin) | 制作画面(Liminalia Studio)だけ(ブラウザ)。「プラグイン」のタブに作品専用の画面を足す |
TyranoScript からの取り込みにかかわるもの(.ks のタグ名、変換、@liminalia/ks-import の import)は ./import だけに書く。
./commands と ./web は書き出すゲームに入るので、@liminalia/ks-import を読まない
(読むと lim build が、どこから読んでいるかを出して止める)。
デスクトップ版のストアの口(GOG Galaxy、Epic Games Store など)は、これらの入口ではなく、liminalia.json の build.store.desktop.module に書く
CommonJS のファイルで差し込む(デスクトップ版のメインプロセスで動く。書き方は distribution.md の「ほかのストア」)。
標準の命令(@liminalia/std)は、書かなくても先頭に入る。同じ名前の命令は後のプラグインが勝つ。
プラグインが読む @liminalia/… は、作品のフォルダに入れていなければ(node_modules が無い作品)、lim と制作アプリが自分のものを使う。
命令(./commands)
命令は CommandDefinition の表で書く。この一つの表から、仕様(lim check の検査と補完、lim reference)と
実行時の処理が作られる。.ks のタグからの変換は書かない(取り込み)。
const stamp: CommandDefinition = {
name: "stamp", // .lim の命令名(英小文字・数字・-)
summary: "画面にスタンプを押す",
positional: ["image"], // 位置引数(名前付きでは書けない)
params: {
image: t.required(t.asset("image", "スタンプの画像")),
x: t.number("中心の横位置(px)", "640"),
wait: t.bool("押し終わるまで次に進まない", true),
},
example: "@stamp stamps/ok.png x=900",
waits: true,
apply(args, extra) { /* stage.extra を書き換える */ },
};
- 引数の型:
t.stringt.numbert.intt.boolt.durationt.colort.enumt.asset(フォルダ)、 キャスト、立ち絵セット、差分、立ち位置は{ type: { kind: "cast" | "appearance" | "variant" | "position" } }。assetは書き出し前に素材の実在を確かめ、ページごとの先読みにも使われる。 - 既定値は、取り込み元の .ks のタグがあるなら、そのタグで省略したときの値と同じにする。取り込みは「既定値と違う値」だけを書き出す。
- waits は演出の終わりを待ってから次へ進むか(
waitを書けばそれが優先)。wait引数を持つ命令は省けばその既定値になる。runで書く命令はctx.present()に渡す待ちと合わせて書く。制作画面の「このページの演出の流れ」とlim referenceがこれを読む。 - 状態は
apply(args, extra)でstage.extraに書く。セーブに入るので JSON にできる素の値だけにし、 キーにはプラグインの名前を付ける("stamp.list")。空になったらキーごと消すとセーブが小さく保てる。 - 結果を変数に入れる、飛ぶ、クリックを待つなど、状態の更新だけで済まない命令は
run(args, ctx)で丸ごと書く (ctx.runtime.jump()/setVariable()/waitClick()、ctx.present()で描画側へ渡す)。 UI を出して待つ命令は、待つ前にctx.resumeHere()を呼ぶと、その間のセーブがこの命令から再開する位置になる。 - 自分の画面で行き先を選ばせる命令(地図、調べる場所など)は、描画側が答えを返さないとき(画面なしで流す: 制作画面の道筋の探索、
網羅、
lim trace)に、await ctx.choose(選択肢の文, 飛び先)で選ぶ。プラグインの中で決まった一つを選ぶと、探索や網羅が ほかの行き先を試せず、その先の行が制作画面で「届かない」ままになる。飛び先("ファイル"か"ファイル#ラベル")を渡すと、 探索が目標に近い行き先から試すconst pick = (await ctx.present(args, true)) ?? { index: open[(await ctx.choose(labels, targets)) ?? 0] }; - 作品のデータで飛び先が決まる命令(調べる場所を選ばせる、地図から行き先を選ばせるなど)は、
targets(args, data)で 飛ぶかもしれない先(#ラベル、ファイル#ラベル)を返すと、制作画面の地図がその矢印を引く(実行には使わない)。argsは台本に書いたままの値(位置引数も名前で引ける)、data(name)は作品のデータ(data/*.json)を読む。 かならずどれかへ飛んで次の行へ進まない命令はleaves: trueを返す({ targets: […], leaves: true })。 - プレイを終える命令(タイトルへ戻る、ゲームを終えるなど)は
ends: trueを書く。制作画面の地図は、この命令で終わる場面に 「終わり」の印を付け、次のラベルへの線を引かない(バッドエンドが別の枝として出る)。命令にending引数を持たせておくと、 台本に書いた値(@title-screen ending="BAD END"など)を印に出す
描画(./web)
export default defineWebPlugin({
name: "stamp",
extensions: {
renderers: { stamp: async (ctx, args, wait, stage) => { /* 描く。wait なら終わるまで待つ */ } },
restorers: [async (ctx, stage) => { /* ロード時に stage だけから組み立て直す */ }],
},
});
- renderer は命令名ごとに一つ。引数は既定値を補ったもの、
stageは命令を実行した後の状態。 見た目は状態と引数だけから決め、自分では進行を持たない。値を返すとrunのctx.present()が受け取る。 - restorer はロードしたときに呼ばれる。演出はせず、
stageから今の姿をそのまま作る。 セーブ、ロードの往復で同じ画面になることを、lim shot --evalでwindow.__liminalia.system.saveTo(1)→loadFrom(1)して確かめられる。 - RenderContext の主な部品:
- 層:
layers.background/sprites/images(カメラの内側)、fx(カメラの外、メッセージ枠の下)、overlay(メッセージ枠の上) asset(種類, パス)、image(url)(読み込みと復号を待つ)、fade(el, to, ms)、audio、loadData(name)(nameはプロジェクトのdata/からのパス。loadData("items.json")とloadData("items")は同じで、拡張子を省くと.json。 JSON として読んで返す)time(ms)はスキップ中に 0 を返す。演出の時間はかならずこれを通すwaitClick()、cast(キャストの定義)、message(メッセージ枠の要素)host.coverUi(): 全画面の動画のように、ゲーム中の操作の UI を覆うものを出している間に呼ぶ。その間システムはクイックメニューと メニューのボタンを出さない(status.menuBarも false)。返す関数で覆いを外す(@movieは再生中にこれを使う。menu=trueなら使わない)studioは制作画面の舞台で動いているとき true。舞台は行を動かすたびにセーブから組み立て直す(restorer も呼ぶ)ので、 ロードの幕のように組み立て直すたびに出ると邪魔な演出は、ctx.studioなら出さない
- 層:
- 前景レイヤー(fg0〜fg2)に独自の種類のものを置くときは、状態を標準の画像の一覧に
kind付きで積み、@liminalia/std/webのregisterLayerItem(kind, build)でロード時の組み立て方を登録する(@free-imageでレイヤーごと消える)。 - 見た目の差し替え:
extensions.text(TextTheme: 書体、既読の表示、クリック待ちの記号、文字の出し方)、extensions.sprites(SpriteTheme: 表情だけの切り替えの時間の倍率)。 - 画像の代わりに描く立ち絵(Live2D、Spine など):
extensions.spritePartsに、キャストの差分のmodel.typeごとの 作り方(SpritePartFactory)を置く。受け取るのは{ who, variant, el, asset }で、el(立ち絵の枠の要素)の中に キャンバスなどを足し、{ update(variant), tick(now, mouth), dispose(), coversImage }を返す。updateは同じ人物の差分が 替わったとき(表情、モーションの切り替え)、tickは毎フレームでmouthはその人物の口の開き(0〜1。ボイスの大きさか、 ボイスが無ければ文字を出している間の開け閉め)。coversImage: trueなら差分のimageは隠す(位置と大きさを決める静止画として使う)。 出す、消す、位置、枠の切り替えはエンジンの立ち絵と同じに動く。描くための外部のライブラリ(Cubism SDK など)は、 作品の側でそのライブラリの使用許諾に沿って入れる。 - システム画面を丸ごと差し替えるときは
system: (env) => SystemControllerを返す。 撮影と書き出し後の確認はboot()、mode、screen、pages、devSkipUntil、startAt()を使うので、この契約は守る。 制作画面(lim studio)の舞台は任意のloadSave(save)を使う。セーブデータから始めて、その位置のページ(か選択肢)を 出し始めたら解決し、終わってもタイトルへ戻らないものにする(既定のシステム画面のloadFromと同じ組み立てで、 スロットを通さずにセーブを受け取る形)。無ければ制作画面の舞台は使えない(プレイヤーは警告を出すだけ)。 命令の行の画面(本文の前)は、プレイヤーがruntime.dispose()で今のプレイを止めてから描画側のrestoreとrestorersだけで 組み立てるので、システム画面に足すものは無い(restorersはロードのとき以外にもこの形で呼ばれる)。 作るRuntimeのonAssignにはenv.onAssignをそのまま渡す。制作画面の舞台で遊んで進めたとき、事実の欄が 舞台で決まった値の出どころ(値を入れた行)を出すのに使う。渡さなくても動くが、そのときは「舞台で決めた」とだけ出て行は出ない。 差し替えない作品にはエンジンの既定のシステム画面が使われる(packages/web/src/system/)。 前の選択肢へ、次の選択肢へは、@liminalia/webのChoiceHistoryとfindChoiceAheadで組める。選択肢を出したとき (setChoosing(true))にhistory.remember(runtime, 履歴の件数)、戻るときはhistory.previous(選んでいる最中か)のpoint.saveから新しいプレイを始め、history.replace(before)で控えを戻す。次へはfindChoiceAhead({ runtime, loadSource, speakers, commands, present })で次の選択肢の時点(save、temp、system)を受け取り、そこから新しいプレイを始める(presentには途中で描画側へ送るものが来るので、 バックログや CG の解放など飛ばしても起こしたいことに使う。無ければ null)。 セーブに残すならhistory.forSave(入れない履歴の件数)を一緒に書き、ロードしたらreplaceで戻す 差し替えたシステム画面でも、遊ぶ人の操作のうち画面の作りによらないもの(指の長押し、なぞり、ゲームパッド、矢印キーで ボタンを選ぶ、指で触れる端末のメニューのボタン、横長の作品を縦に持ったときの知らせ)は@liminalia/webのbindPlayerInput({ stage, controls })で使える。controlsに「遊んでいる最中か」「矢印キーで選ぶ範囲」と、 メニュー、履歴、枠を隠す、画面を閉じる、設定、押している間スキップ などの関数を渡す(渡さなかった操作は何もしない)。 自分でキーを振り分けるなら、その受け口より先に呼ぶ。既定のシステム画面もこれを使う(操作の一覧は docs/player.md)。 setup(host)は描画側を作った直後に一度だけ呼ばれる(書体の読み込みなど)。- 書き出したあとの確かめ(
lim build)とlim shotは、タイトル画面が出てからbuild.verifyWait(既定 2500 ミリ秒)待って写真を撮る。 白からの長いフェードで始まるタイトルなど、撮ってよい時を作品が決めるなら、const release = host.holdShot()で止めておき、 撮ってよくなったらrelease()を呼ぶ(呼ぶまで撮らない。60 秒で諦めて撮る)。 - 標準の操作を作品のボタンから呼ぶ:
ready({ host, system })がシステム画面を作った直後に一度だけ呼ばれるので、systemを取っておき、system.perform("auto")のように呼ぶ。操作はSYSTEM_ACTIONS(quick-savequick-loadsaveloadautoskipprev-choicenext-choicebacklogconfighidetitleexit。ゲーム中の帯のボタンと同じ動きで、title、exit、prev-choiceは確かめてから行う)。 遊んでいないとき(タイトル、システム画面を開いているあいだ)は何もしない。オート、スキップ、枠を隠しているかはsystem.togglesで読める(作品のボタンの見た目を合わせる)。標準の帯を使わないなら liminalia.json のui.menuBar.positionをhiddenに、指で触れる端末のメニューのボタンも要らなければui.menuBar.touchButtonをfalseにする。 帯をシナリオの途中で出し入れするなら@menu-button(状態に残り、ロードしても同じになる)。 - 見た目を作品の CSS で変える: エンジンの CSS はすべてカスケードレイヤー
liminaliaに入っている(@layer liminalia { … })。 作品の CSS はレイヤーの外に書けば、セレクタの強さを競わずに勝つ(.stage .message …のように強めなくてよい。.nvl-page { margin: 0 }で効く)。 作品の CSS を読み込む場所はsetup(host)で<style>を足すか、プラグインの./webからimport "./my.css"。 変えたい値はなるべく CSS 変数で変える(舞台の要素に書けば中のすべてに効く):- 本文の枠:
--message-left--message-top--message-right--message-bottom--message-width--message-height--message-min-height(ゲームの設定のui.messageもこれで渡る)、--message-padding--message-bg--message-image--message-border--message-radius、 縦書きの枠の幅--vertical-width、NVL の余白と背景--nvl-padding--nvl-bg - 本文の文字:
--font--text-size--text-line--text-letter-spacing--text-shadow--text-align--message-fg--read-fg、 ルビ--ruby-size--ruby-gap、段落の間--paragraph-gap、一行の字数--line-chars、禁則--line-break、ぶら下げ--hanging - 名前の欄
--name-fg--name-size--name-bg--name-image--name-border、クリック待ちの印--glyph-fg--glyph-size--glyph-gap、 選択肢--choice-*、吹き出し--balloon-*、標準のシステム画面--lm-*(docs/player.md) エンジンの CSS は@layerに対応したブラウザで動く(Chrome、Edge、Android の WebView 99 以降、Safari、iOS 15.4 以降、Firefox 97 以降)。
- 本文の枠:
- 作品の画面(捜査、地図、推理、原作のセーブ画面など)は
system.openScreen(content, options)で開く。見た目(要素と CSS、画像)は 作品が作り、システムが決まりごとを受け持つ:- 標準のシステム画面と同じ層、同じ重なりの並びに置く(後に開いたものが上。
system.screenはその名前) - 下にある入力を止める。本文送りのクリックとホイール(履歴、送り)は画面の中で止まり、右クリック、Esc、ゲームパッドの B は
この画面の「戻る」になる(
closeOnBack: trueなら閉じて結果はnull、そうでなければ何もしない)。オート、スキップは閉じるまで止まる pauseScenario(既定true)なら、開いている間シナリオの次の命令を実行しない(標準の画面も止めるならui.screen.pauseScenario)- 閉じたら
handle.result(Promise)がhandle.close(値)の値になる keyを書くと、状態(handle.state、handle.setState(新しい状態)。JSON にできる値)がセーブに入る。同じkeyで開き直すと セーブの状態から始まるので、命令の中で開く画面(命令はctx.resumeHere()を呼ぶ)は、ロードで命令がもう一度開けば元に戻る。 命令の外(作品のボタンなど)から開く画面は、system.defineScreen(key, 作り方, options)で作り方を登録しておくと、 ロードしたときにシステムが同じ状態で開き直す。stage.extra への記録と restorer を画面ごとに書かなくてよい
const h = system.openScreen((h) => buildMap(h), { key: "gung.map", state: { page: 0 }, closeOnBack: true }); const picked = await h.result; // close(値) の値。戻る・ロードで捨てたら null - 標準のシステム画面と同じ層、同じ重なりの並びに置く(後に開いたものが上。
- 見た目を持たない UI 部品(
import { ui } from "@liminalia/web")は、作品が作った要素に動きだけを付ける。状態は要素の属性に出すので、 見た目は作品の CSS で決める(data-lm-hover、data-lm-focus、data-lm-pressed、data-lm-disabled、data-lm-selected、data-lm-on、aria-pressed、つまみの位置--lm-value(0〜1))。押したクリックは本文の送りへ流さない。システム画面の外でも使えるui.button(el, { onPress, disabled, hit, se, hoverSe, tapToSelect, label }): 押せる範囲hitは要素の矩形(既定)、{ x, y, width, height }(要素の左上から px)、{ alpha: true, threshold }(画像の透明でない部分。imgか背景画像、imageで別の画像)。 重なっていれば押せる範囲に入っている一番上のものを押し、透明な部分は下のボタンへ通す(ボタンでない要素は通さない)。<button>以外の要素はキーボードで選べるようにし(Tab、矢印キーで選び、Enter、Space で押す)、tapToSelectは指で触れる端末で 一度目のタップで選び二度目で押す。se、hoverSeは sound からの効果音ui.slider(el, { min, max, step, value, onChange, onCommit, orientation, thumb, setting }): ドラッグ(指も)、矢印キー、 Page Up/Down、Home/End で動かし、動かしている間は値が変わるたびにonChange。<input type="range">ならその動きを使うui.toggle(el, { value, onChange, setting, se })(入、切)、ui.choice([{ el, value }, …], { value, onChange, setting })(一つを選ぶ)setting: { system, key }で設定に結び付ける(keyはbgmなどか、作品独自の値の"custom.名前")。変えたらsystem.updateSettingsで保存してすぐ反映し、ほかで変わったら合わせる- 返す扱いの
dispose()で外す(要素を捨てれば一緒に消える)
- 標準の画面の見た目だけを差し替える:
screens: { config?(api), saveload?(api), backlog?(api), menuBar?(api), menu?(api), confirm?(api), extra?(api), flowchart?(api) }で要素を作る関数を渡す。開く、閉じる、 入力を止める、重なりは標準の画面と同じで、中身はその関数が作る。apiは設定画面がsystem(設定はui.sliderなどのsettingに 結び付けられる)、languages、close、exportSaves、importSaves、セーブ、ロードがmode、system(slots、slot、deleteSlot)、slotCount、lastSlot(最後にセーブした枠)、inGame、save(slot)、load(slot)、ask(message, kind)、notify(message)、close、履歴がentries、playVoice(i)、jump(i)、color(speaker)、close、フローチャートがflow(場面とつながり)、title(scene)、reached(scene)、canJump(scene)、jump(scene)、readRate、endings、close、onBack。CSS だけで変えるなら要らない(標準の画面も中はこの部品で組んである)- 操作の帯
menuBar(api): 一度だけ作り、システムが置いて、帯を出す場面(status.menuBar: 遊んでいて、画面を開いておらず、 枠を隠しておらず、メッセージ枠が出ていて、選択肢を出しておらず、全画面の動画などが覆っていない)だけ見せる。apiはitems(ui.menuBar.itemsか既定の並び)、label(操作)、perform(操作)、status、onStatusChange(オート、スキップの押された見た目を合わせる) - メニュー
menu(api)(右クリック、Esc、パッドの START。作品のボタンからはsystem.perform("menu")):items、label、perform(操作)(閉じてからする)、status、close - 確かめ
confirm(api):kind(title、exit、end-replay、backlog-jump、prev-choice、import、delete、overwrite、load、 作品の画面のask(message, kind)の名前)、message、yes、no、answer(はいなら true)。Esc、右クリックは「いいえ」 - おまけ
extra(api):extra(liminalia.json の extra)、tab、解放の具合(cgImages(cg)解放した画像、replayUnlocked、musicUnlocked、achievement)、asset、audio、replay(回想)、close、onBack(handler)(Esc で先に閉じるものがあれば true を返す)。 CG のgroup(章などの見出し)とdescription(説明)は一覧に使える
- 操作の帯
- 標準の画面の文言を替える:
texts: { "confirm.title": "タイトル画面に戻ります。よろしいですか?", … }(鍵はsystem/strings.tsのTEXTの入れ子。言語の訳のuiより後に重ねる) - 起動の演出:
splash(ctx)を返すと、「クリックで開始」のあと、タイトルの前に一度だけ流す(済んだら解決する Promise) - 様子と出来事(
readyで受け取ったsystem。作品が自分で置くボタン、帯の出し入れや、作品の演出を差し込むのに使う):system.status(mode、screen、choosing、hidden(遊ぶ人が枠を隠している)、window(メッセージ枠が出ている)、waiting(クリック待ち)、auto、skip、replay、menuBar、canPrevChoice、canRollback、canRollforward)、system.onStatusChange((s) => …)(変わったときだけ)system.onEvent((e) => …):start(「はじめから」でプレイを作った直後。e.runtimeに変数を入れる、舞台を整えるなど)、load-start(ロード、履歴から戻る、前の選択肢で今のプレイを捨てる直前。幕を下ろすなど)、screen-open/screen-close(nameと開いたときのfrom)、rollback(巻き戻しで組み立て直す直前。directionはback、forward。ロードではないので 幕は下ろさない)、title。startとload-startは、返した Promise が済むまで進まないsystem.systemVariables(全セーブ共通の変数。タイトルでも読める)、system.importData({ slots, system, read, settings })(前の保存の形からの引き継ぎ。スロット、共通の変数、既読、設定を書き、書けなかったスロットの数を返す)
- 作品の設定の幅と入力(liminalia.json の ui):
ui.settings(textSpeedMin、textSpeedMax、textSpeedStep、autoWaitMin、autoWaitMax、autoWaitStep、autoPerChar)で設定の幅とオートの待ちを、ui.input.rightClick: "hide"で右クリックを 枠を隠す操作に、ui.title.bgmVolumeでタイトルの BGM の音量を変える。遊んでいる間、選択肢を出している間は舞台にlm-choosing、枠を隠している間はlm-window-hiddenのクラスが付く - 設定、履歴、セーブの枠を作品の画面から使う(
readyで受け取ったsystem。既定のシステム画面が持つ):- 設定:
system.settings(読むだけ。文字の速さtextSpeed、オートの待ちautoWait、音量bgmsevoice、未読も飛ばすskipUnread、次のページでボイスを止めるvoiceStopOnPage、話者ごとの音量speakerVoice、全画面fullscreen、言語language)、system.updateSettings({ bgm: 60 })(幅に収めて保存し、すぐ反映。変えたあとの設定を返す)、system.onSettingsChange((s) => …)(設定画面、@config、全画面の切り替えでも呼ぶ。返す関数で外す)。 数値の幅はSETTING_RANGES。作品独自の設定はcustomに入れる(updateSettings({ custom: { windowAlpha: 0.8 } })。 名前ごとに重ね、undefinedで消す。同じ場所に保存され、エンジンは読まない) - 履歴:
system.backlog(古い順。話者speaker、出した名前label、本文の一片parts(文字text、ルビruby、太字、斜体、色、 改行{ br: true })、ボイスvoice、位置at、読み直せるかcanJump)、system.playBacklogVoice(i)、system.jumpToBacklog(i)(確かめてから、その項目から読み直す) - セーブの枠:
system.slots({ from, to })(番号つきの枠。空の枠も)、system.slot("quick")。日時date、本文の頭title、 縮小画像thumb、作品のメタ情報meta。メタ情報はプラグインのslotMeta({ runtime, host })がセーブのたびに返すもの (標準のセーブ画面からのセーブにも付く)に、system.saveTo(slot, meta)のmetaを重ねる。system.deleteSlot(slot)で消す。 セーブを守る設定(saves.protect)でも同じに読めるので、localStorage を直に読まない
- 設定:
- タイトル画面だけを差し替える:
title: (api) => HTMLElementを返す。返した要素を舞台いっぱいに置き、ほかのシステム画面 (セーブ、設定、履歴など)は標準のまま使う。apiは標準のタイトルのボタンと同じ動き(start()はじめから、load()つづきから (一覧を開く)、loadLatest()一番新しいセーブから、hasSave()、config()、extra()、hasExtra()、exit()、canExit())とproject、host(素材のパスはhost.context.asset(種類, パス))、ask(message, kind)(標準の確かめ)、intro(起動して最初のタイトルか。導入の演出を出すかどうかに使う)。タイトルを出すたびに呼ぶ(言語を替えたときも作り直す)。 背景と BGM はui.titleのものを標準どおりに出す。システム画面を丸ごと差し替えた作品では効かない。
取り込み(./import)
TyranoScript の作品を lim import で取り込むときの設定。lim import は、組み込みの標準の命令の変換表に、
plugins の並びのとおりプラグインごとの ./import(と import.tyrano.profile が指すパッケージの ./import)を重ねて使う。
命令の仕様(引数の型、位置引数、既定値)は ./commands の命令の表から引くので、ここには .ks との対応だけを書く。
const profile: TyranoImportProfile = {
mappings: [
// [stamp_show storage=… x=…] を @stamp にする。.ks の storage は @stamp の image
{ command: "stamp", tags: ["stamp_show"], params: { image: "storage" } },
{ command: "stamp-clear", tags: ["stamp_clear"] },
],
};
export default profile;
mappings(TagMappingの並び): .ks のタグから命令への変換表。commandは変換先の命令名、tagsは変換元の .ks のタグ名。paramsは「.lim の引数名 → .ks の引数名」。書かなかった引数は名前の-を_にしたもの、nullは .ks に対応なし。- 書かなければ機械的に変換する(.ks に書かれていた値のうち、命令の既定値と違うものだけを書き出し、変換しなかった引数は診断に残す)。
- 機械的に変換できないときは
convert(pm, tag, ctx)。ctx.specは命令の仕様、ctx.mechanical(pm)は値を整えてから 機械的に変換するとき、ctx.convertAs(命令, pm, タグ)はほかの変換表に任せるとき。null を返すと機械的な変換になる。 - 組み込みの変換に任せたい書き方は
defer(pm)([jump]のうち特定の行き先だけを自分の命令にするなど)、 変換しない .ks の引数はignore、直後の[s]を取り除くときはswallowStop。
scriptRules/scriptProfile:[iscript]の JS を命令に置き換える(置き換えた .lim の行を返す。当てはまらなければ null)unsupportedTags: 扱わないタグとその理由(取り込むと理由付きのエラーにし、lim referenceにも載る)tags: 変換表で書けないタグの変換(タグ名 →(pm) => TagConversion | null。変換表より優先する)plugins: 働きを引き受ける TyranoScript のプラグイン(JS があっても手作業として報告しない)skipFiles: 本文として変換しない .ks(タイトル画面など、システム画面が持つもの)afterImport(ctx): 作品固有のデータ(data/以下の JSON)を書き出し、キャストを返す (Tyrano Tatie と同じ形の設定を別の名前で持つなら、castFromTatieSettingsでキャストにできる)writeCast(ctx): 立ち絵の位置調整画面で保存したキャストを、取り込み元へ書き戻すbackport: 書き戻し(lim backport)で、命令の行を .ks のタグに戻す逆向きの変換(命令名 →(行, 元のタグ) => タグの字面 | null)。 書き戻しは変換表と元のタグを手本に引数を書き換えて探すので、ふつうは要らない。tagsで書いた変換のように手本だけでは戻せない命令に使う (tyrano-backport.md)
標準の命令(@image など)の変換表は @liminalia/ks-import の STD_MAPPINGS(packages/ks-import/src/std-tags)にある。
変換表の書き方の例として読める。
制作画面(./studio)
作品専用の管理画面(設定の表、データの編集など)を、制作画面の「プラグイン」のタブに足す。制作画面(開発サーバー)だけが読み、
書き出すゲームには入らない。タブの左の欄に、プラグインの名前(name)ごとにまとめて、views の順に並ぶ。
新しいプラグインは「プラグイン」のタブの「新しく作る」で、動く例の入ったひな形から始められる。
import { defineStudioPlugin } from "@liminalia/studio/plugin";
export default defineStudioPlugin({
name: "stamp",
views: [{ id: "stamp-book", title: "スタンプ帳", mount: (el, ctx) => { /* el の中に組み立てる */ return { flush, refresh }; } }],
});
mount(el, ctx)ははじめてタブを開いたときに一度だけ呼ぶ。返すもの(任意):show、hide(タブの出入り)、flush(保存待ちをすぐ書く。 ウィンドウを閉じる前、⌘S、「更新」の前に呼ぶ。書けなければ理由の文字列で拒否する)、refresh(「更新」で作品を読み直したとき)ctxの口:readData(name)、writeData(name, text, baseHash): 作品のデータ(paths.dataの .json)。読んだときのhashをbaseHashで返し、 ほかで書き換えられていたら書かずに{ ok: false, current }を返す(今の中身に重ねて書き直す、など画面が決める)。 前の版は.liminalia/history/@data/に残るonDataChanged(listener): データがほかで変わった(ほかのエディタ、AI、git)。onProjectChanged(listener): ファイル、キャスト、素材の一覧が変わったproject(題、画面の大きさ、ファイル、キャスト、素材の一覧)、assetUrl(kind, path)、uploadAsset(kind, path, file, overwrite)(画像、音、動画、書体だけ)openScript(file, line)(編集のタブで台本のその行を開く)、notify(text)(下の帯のお知らせ)
- 見た目は制作画面の CSS の変数で揃える(色
--sheet、--paper、--rule、--ink、--ink-soft、--cue、--cue-wash、--markなど、 文字--fs-s(12px)、--fs-m(14px)、--fs-l(20px、見出し)、--fs-xl(36px)、線--line、角丸--r-s、--r-m、--r-l。 どれも Radix Colors の段を指し、暗い配色でも効く。灰は mauve、操作と現在地は violet。影は使わない)。 ボタン、入力欄、選択欄は制作画面の既定の見た目がそのまま効く(button.primary、button.ghost) - 欄の幅を境目のドラッグで変えるつまみ
Splitterも@liminalia/studio/pluginから使える(入れ物の CSS の変数に px で入れて覚える。new Splitter({ host, side: "left", variable: () => "--list-w", min: 200, max: (w) => w - 480, storageKey: (v) => \作品.画面${v}` })。 つまみの見た目は.splitter(position: absolute。置き場所は画面の CSS でleft: calc(var(—list-w) - 4px)` のように決める) - 例は examples/stamp-plugin/src/studio.ts(素材の画像ごとのメモを
data/stamps.jsonに書く)
確かめ方
lim check # 命令名・引数・型・キャストの誤りを、位置と直し方付きで
lim reference # 自分の命令がリファレンスにどう載るか
lim trace <ファイル> # 画面なしで流し、状態(stage.extra)を JSON で見る
lim shot <ファイル> --skip N --settle # 指定ページの画面写真
単体テストは、combinePlugins([plugin]) の implementations を Runtime に渡し、HeadlessHost で流して
runtime.state.stage.extra を確かめるのが手早い。取り込みは、specs から作った registry と ./import を
resolveImportProfile に渡し、その tags を convert の profile にして確かめる
(examples/stamp-plugin/test)。