Skip to content

Graphs ja

Tom_XV edited this page Sep 23, 2026 · 8 revisions

English | 日本語

実験的な機能です。 フレームワーク 1.4.1 で入ったもので、この先のバージョンで変わったり、無くなったりするかもしれません。

Graphs(DragNWash.ModFramework.Graphs、バージョン 1.5.0)は、コードを書かずに「何かをする」Mod を動かすためのライブラリです。これが起きたら、これをする という形で書きます。たとえば「シーンが読み込まれたら、1 秒待って、何があるか数えて、ログに 1 行書く」とか、「この会話のノードが始まったら、あのオブジェクトを隠す」といった具合です。グラフは mod.json のあるフォルダーに置く JSON のファイルで、Overrides の Mod と同じ形なので、1 つの Mod に両方入れても構いません。コンパイルは要らず、あなたのコードが走ることもありません。グラフが呼べるのは各ライブラリが登録した操作だけで、Console の op コマンドや Bridge で使えるものと同じです。

グラフは、Bridge のページにあるエディターで作ります。コードのグラフの隣で、Scratch のようにブロックを並べます。

設計と調査の経緯は docs/GRAPHS.ja.md にあります。

遊ぶ人向け

グラフの Mod を入れる

グラフの Mod は、1 つのフォルダーです。ゲームの BepInEx/plugins フォルダーに入れて、次のような形にします。

BepInEx/plugins/Scene notes/
  mod.json
  graphs/scene-notes.json

フレームワークと Graphs ライブラリが要ります。グラフを 動かす だけなら Developer tools は要りませんが、作る ときはエディターが Bridge のページにあるので要ります。ゲームを起動すると、その Mod がほかの Mod と同じように、名前と作者、説明つきで Mods 画面(Options → Mods)に並びます。

動く前に、そのグラフが何をするか

Mod の詳細には Graphs のタブがあって(1.5.0。それまではボタンで開くページでした)、グラフごとに何をするかが言葉で並びます。

Scene notes (graphs/scene-notes.json)
  Answers: scene loaded, save written
  Reads:   inspector.objects.children, saves.flags.list
  Changes: nothing
  Needs:   Inspector

大事なのは Changes の行です。そのグラフが何かを変えるのに使うかもしれない操作が、全部ここに並びます。動く前にファイルから読み取ったものなので、Changes: nothing と出ていれば、そのグラフにできるのは見ることとログに書くことだけです。動いているかどうかや、何回始まったかもここに出ますし、Stop for this session のボタンを押せば、そのグラフを止めて、変えたものを元に戻せます。

入っていないライブラリ(たとえば Inspector)を使うグラフは、needs Inspector と出て動きませんが、そのライブラリを入れれば動き始めます。

オフにする

  • 1 つのグラフを今すぐ止めるなら、Mod の Graphs タブの Stop for this session を押すか、Console で graphs stop <ファイル名> と打ちます。変えたものは元に戻ります。
  • 1 つの Mod を止めるなら、ほかの Mod と同じく Mods 画面でオフにします。効くのは次にゲームを起動したときからです(mod.json の名前が mod.json.disabled に変わります)。
  • グラフをまとめて止めるなら、Mods → Drag'n Wash ModFramework: Graphs → Settings → [General] Enabled です(初期設定はオン)。
  • 消したいときは、その Mod のフォルダーを削除します。

グラフにできないこと

これは「しない約束」ではなく、そもそも仕組みとしてできないようになっています。グラフには、ゲームのメソッドを名前で呼ぶことも、リフレクションも、自前のファイルの読み書きも、通信も、プログラムの起動もできませんし、いつまでも走り続けることもできません。できるのはグラフ向けに登録された操作を呼ぶことだけで、グラフが新しく足すのは いつ やるか、だけです。変えたものは、止めたときや読み込み直したとき、オフにしたときに元へ戻り、セッションを越えて残るものはありません。このバージョンのグラフは、セーブデータにも触れません。

3 回続けて失敗したグラフは、そのセッションのあいだオフになります。変えたものは元に戻り、Mods 画面に印が付きます。例外を投げ続ける Mod のイベントハンドラーと同じ扱いです。

Mod を作る人向け

フォルダー

BepInEx/plugins/<Mod>/
  mod.json              名前、作者、説明、バージョン(overrides と同じ)
  graphs/*.json         グラフ 1 つにつき 1 ファイル
  overrides/*.json      任意:1 つの Mod に両方入れられます

mod.json は Overrides のページにあるものと同じです。どちらのデータ Mod も中核にある 1 つのローダーが見つけるので、両方入ったフォルダーは Mods 画面では 1 つの Mod として出て、オフにするのも 1 回で済みます。

グラフのファイル

{
  "format": 1,
  "name": "Scene notes",
  "description": "シーンの最上位に何があるかをログに書きます。",
  "variables": { "scenes": 0 },
  "on": [ { "id": "h1", "event": "scene.loaded", "do": [] } ]
}
キー 意味
format 必須 1。これより新しい形式のファイルは読まず、フレームワークを更新するように言います
name、description 任意 Mods 画面とエディター用。無ければファイル名
variables 任意 グラフの変数と最初の値(文字列・数値・true/false・null)。ゲームが動いているあいだだけ生き、保存はされません
on 必須 答えるイベント=ハンドラーの一覧
layout 任意 エディターがブロックを置いた場所。ゲームは読みませんし、いまのエディターは書きません

ほかのキーはエラーになるので、書き間違いがあっても黙って無視されず、読み込んだ時点で分かります。ハンドラーと文にはファイル内で重複しない id が必ず付きます(エディターは h1、s12 のように付けます)。エラーもログも layout も文を id で指すので、ブロックを動かしてもずれません。

ハンドラー

{ "id": "h1", "event": "scene.loaded", "when": true, "overlap": "skip", "do": [] }

when は任意の絞り込みで、真のときだけ始まります。overlap は、実行が終わらないうちに同じイベントがまた来たときの動きです。skip(初期値)なら無視して、queue ならもう 1 つ始めます。1 つのグラフで同時に走れるのは 8 本までです。

グラフが答えられるイベントは次のとおりです。

イベント 渡される値 出どころ
game.started 中核
scene.loaded scene、mode 中核
scene.unloaded scene 中核
dialogue.node.started node Dialogue
dialogue.line.showing line_id、speaker、text Dialogue
dialogue.option.showing line_id、text Dialogue
saves.written slot Flags and saves
timer.every Graphs ライブラリ:seconds(0.5 以上)ごと、実時間
key.pressed key Graphs ライブラリ:指定したキー(F8 など)。F1 は使えません

キーは誰のものでもありません。 ほかの Mod が同じキーに設定を持っていれば、両方が反応します。1.4.2 からは、ほかに誰がそのキーを使っているかを、グラフがログと Console の graphs、その Mod の Graphs タブで教えてくれます。たとえば F6 is also Drag'n Wash Localization: [Debug] DumpDialogueKey; both answer it のように出ます。1 つのキーに 2 つの仕事をさせたい人もいるので、拒否はしません。その行を読んで、意図しない重なりなら別のキーにしてください。なお Drag'n Wash Localization は F6 と F7 をダンプに使っています。

実行はイベントの中では始まりません。値を写しておいて、ライブラリの次のフレームから始まります。そのためグラフが、ほかのライブラリのフックの途中(会話の行を出している最中や、シーンが半分できたところ)で動くことはありませんし、遅いグラフがほかの Mod のイベントを遅らせることもありません。代わりに 1 フレーム遅れるので、行が出る前に書き換えることはできません。それは書き換え役(GameText.AddRewriter、Text)の仕事です。

文

文は id と、次のキーのうちちょうど 1 つを持つオブジェクトです。

文 書き方 意味
call {"call": "saves.flags.list", "args": {}, "as": "flags", "onError": []} 操作を呼びます。名前はそのまま書くもので、実行中に組み立てることはできないので、何を呼ぶかは動く前に分かります。as は結果に名前を付け、同じ実行の後の文から使えます。onError が無ければ失敗した呼び出しは実行全体の失敗で、あれば代わりにその中の文が動き、error にメッセージが入ります
set {"set": "count", "to": 1} variables にある変数に入れます
if {"if": true, "then": [], "else": []}
wait {"wait": 1.5} 実時間の秒数、0〜600。0 は次のフレーム
repeat {"repeat": 5, "do": []} 1〜1000 回
while {"while": true, "max": 100, "do": []} 真のあいだ、最大 max 周(必須)。max 周のあとも真なら失敗です
each {"each": [], "as": "item", "max": 50, "do": []} リストの要素ごとに 1 回。max より長いリストは失敗です
log {"log": "hello", "level": "Info"} その Mod の名前でログに 1 行(Info、Warning、Error)。1 グラフにつき毎秒 20 行まで
stop {"stop": true} この実行を終えます(グラフは止まりません)

goto も、ほかのハンドラーを動かす文も、再帰もありません。実行は必ず終わり、どれくらいで終わるかは下の上限で決まります。

式

そのままの値("text"、3、true、null)か、演算子を 1 つ持つオブジェクトです。

{"var": "name"} 変数、またはこの実行で先に as が付けた結果(onError の中では error)
{"event": "scene"} イベントが渡した値
{"get": [{"var": "root"}, "children"]} オブジェクトの項目、リストの要素(0 が先頭、-1 が末尾)。無ければ null
eq、ne、lt、le、gt、ge {"gt": [a, b]}。数どうしは数として、ほかは文字列として比べます
add、sub、mul、div 数だけです。数でないもの、0 で割ることは失敗です
and、or、not {"and": [a, b]}、{"not": a}
join {"join": ["Scene ", {"event": "scene"}]}。リストやオブジェクトは JSON として書かれます
contains 文字列の中の文字列(大文字小文字は区別しません)、またはリストの中の要素
length 文字列・リスト・オブジェクトの長さ。ほかは 0

false、0、""、null、空のリストと空のオブジェクトは偽として扱われます。引数は Console と同じように変換されるので、数のところに "3" と書いても通ります。

例

{
  "format": 1,
  "name": "Scene notes",
  "description": "シーンの最上位に何があるかをログに書きます。",
  "variables": { "scenes": 0 },
  "on": [
    {
      "id": "h1",
      "event": "scene.loaded",
      "do": [
        { "id": "s1", "set": "scenes", "to": { "add": [{ "var": "scenes" }, 1] } },
        { "id": "s2", "wait": 1 },
        { "id": "s3", "call": "inspector.objects.children", "as": "roots" },
        { "id": "s4", "log": { "join": ["Scene ", { "event": "scene" }, ": ", { "length": { "var": "roots" } }, " root objects"] } },
        { "id": "s5", "each": { "var": "roots" }, "as": "root", "max": 200, "do": [
          { "id": "s6", "if": { "gt": [{ "get": [{ "var": "root" }, "children"] }, 50] }, "then": [
            { "id": "s7", "level": "Warning", "log": { "join": [{ "get": [{ "var": "root" }, "path"] }, " has ", { "get": [{ "var": "root" }, "children"] }, " children"] } }
          ] }
        ] }
      ]
    }
  ]
}

ブロックで見ると、このハンドラーは帽子ブロックの下の 1 本の積み重ねです。

シーンが読み込まれたら
  scenes を (scenes + 1) にする
  1 秒待つ
  roots = inspector › objects › children
  ログ "Scene " (scene) ": " (roots の長さ) " root objects"
  roots の root ごとに(最大 200)
    もし (root › children) > 50 なら
      警告ログ (root › path) " has " (root › children) " children"

グラフが呼べる操作

グラフが呼べるのは、操作の登録簿にある 読み取り の操作のうち、ページ専用のもの(code.*、graphs.*)以外の全部です。いまのところ、中核の mods.list・game.info・scene.list・mods.network、Tool window の log.read、Assets の一覧、Dialogue の dialogue.current・dialogue.recent、Text の text.rewriters・text.shown、Flags and saves の saves.list・saves.flags.list・saves.flags.get、Inspector の inspector.objects.find・inspector.objects.children・inspector.components.list・inspector.member.get・inspector.selection.get です。

それに加えて、Overrides ライブラリの 書き込み の操作 3 つも呼べます。overrides の 1 行がすることを、グラフが選んだタイミングでやるものです。

操作 何を 引数
objects.member.set コンポーネントのフィールドやプロパティ path、component、member、value。ほかに index、private、scene
objects.material.set マテリアルのシェーダープロパティ path、material、property、value。ほかに component、index、scene
objects.active.set オブジェクトの表示・非表示 path、active。ほかに scene
objects.writes(読み取り) このライブラリが変えたものと、誰が頼んだか。同じところを変えた別の Mod も出ます

値は文字列で、Overrides のページの表のとおりに書きます(2.5、true、#RRGGBB、1, 0.5, 2)。書き込みはそれぞれ、書く前の値を覚えていて、グラフを止めたときや読み込み直したとき、失敗したときに、新しいものから順に戻します。ログには graph:<mod>/<ファイル> という名前で Info として残ります。Inspector の History には、手で直した分と並んで「誰が変えたか」つきで出ます。その行から、グラフを止めずに 1 件だけ戻すこともできます。(Undo last と Ctrl+Z は自分の編集だけが対象で、ほかの Mod の変更は飛ばします。Export as overrides にも入りません。)

グラフが呼んだものは、どれもセッションを越えて残りません。セーブデータにフラグを書く操作(saves.flags.set)は、ファイルを変えるもので別に設計が要るので、今はわざと出していません。

自分の Mod で操作を登録すれば(操作の登録簿)、こちらを直さなくてもグラフから呼べるようになります。こうして Mod は、コードを書かない人がその上に何かを作れる足場を用意できます。

エディター

エディターは、Bridge がこのパソコンの中で出しているページの Graphs タブです。F1 のウィンドウの Bridge タブ(Developer tools)で Bridge をオンにし、そこの Graphs を押すと、エディターを開いた状態でページが出ます。隣の Open page を押すと、同じページがコードのグラフの表示で開きます。2 つの表示はページの中で切り替えられます。

  • 左は読み込まれているグラフの一覧と、New graph です。エディターの外でファイルが消されたグラフは、開こうとして失敗するのではなく、一覧でそうと分かるようになっています。Reload でファイルを読み直すと、一覧から消えます。
  • 中央はグラフをブロックで見せます。ハンドラーごとに帽子ブロック、文ごとにブロック、call には操作の引数が差し込み口として並びます。中身は登録簿から来るので、どのライブラリが足した操作でも、引数と説明と種別つきでそのまま出ます。書き込みの操作は色が違います。
  • 入力するたびに、ゲームと同じ規則で 検査 します。問題は文の id つきで並びます(s2: objects.active.set needs path)。そのグラフが何を変えるかも、Mods 画面と同じ言い方で出ます。
  • グラフのブロックには、名前、説明(Mods 画面に出ます)、そして変数が並びます。set 文はここに並んだ名前しか使えないので、変数の追加や名前の変更、削除、初期値の指定はここでします。
  • ハンドラーのブロックには、イベントに加えて only if(真のときだけ実行)と again while running(skip か queue)があります。
  • 値が色ならカラーピッカーが付きます。objects.member.set の場合は member の型をゲームに聞いて、欄の横に Color や Single と出します。ピッカーをドラッグすると、その色がそのままゲームに反映されます(グラフがするのと同じ書き込みなので、Inspector の History に並び、そこから戻せます)。
  • Save は、データ Mod の graphs/ フォルダーにファイルを書きます。Mod がまだ無ければ mod.json ごと作り、置き換えたファイルは <名前>.json.bak として残してから、グラフを読み込み直します。BepInEx/plugins/<フォルダー>/graphs/ の外には書きませんし、DLL のあるフォルダーには書きません。
  • Run は、隣に表示されているハンドラーを、イベントを待たずに始めます(先頭に限りません)。止めてあるグラフは Run で動き直します。Stop はグラフを止めて、変えたものを元に戻します。
  • Rename はファイルを移動します(複製は残りません)。Save はいつも、グラフがいまある場所に書きます。Delete はそのグラフを Mod から外し、ファイルを <名前>.json.bak として同じ場所に残します。
  • Log の欄には、ライブラリのログがそのまま流れます。Clear でこの欄だけ空にできます(ゲーム側のログは触りません)。

バーの右にある Blocks と Nodes は、同じファイルの 2 つの見せ方です。ノード表示は、ハンドラーと文を箱にして描きます。流れは辺をたどって下りていき(next、then、else、do、on error)、as で名前を付けた結果は、それを読む文への破線のワイヤーになります。書き込みの操作は色が違います。形を眺めるための表示で、ノードをクリックするとそのブロックが開きます(項目はそちらにあります)。

ブロックは頭をつかんでドラッグします。別のブロックの上や下、並びの末尾の + add の行に落とすと、そこへ移ります(if の then の中へ、ループの外へ、文が置けるところならどこへでも)。ハンドラーはハンドラーの並びにしか入りませんし、自分の中には落とせません。↑ ↓ ✕ のボタンも今までどおり使えます。

ページは窓の幅に合わせて組み替わります。狭いときは Problems と Log がブロックの下に回り、さらに狭いと 1 カラムになります。

このページはすべて自前のコードで描いています。インターネットから何も取ってきませんし、ライブラリも同梱していません。

Console

Tool window が入っていれば、Console に次が増えます。

コマンド 動き
graphs グラフごとに、答えるイベント・読むもの・変えるもの・必要なもの・問題・動き具合
graphs reload 変えたものを戻し、ファイルを読み直して動かします。ゲームを動かしたままファイルを編集するとき用
graphs stop <ファイル名か名前> グラフを 1 つ、このセッションのあいだ止めて、変えたものを戻します

C# からは GameGraphs.Loaded、GameGraphs.Reload()、GameGraphs.Stop(which)、それに [BepInDependency] 用の GameGraphs.Guid が使えます。

上限

グラフはゲームの 1 フレームをほかのものと分け合うので、次の上限があります。

グラフ全体 1 フレームに 1 ms([Graphs] FrameBudgetMs)。使い切ったら次のフレームに続きます
1 実行あたり 10,000 ステップ、ループは最大 1,000 周、待ちは最大 600 秒
1 グラフあたり 同時に 8 実行、ログは毎秒 20 行
1 ファイルあたり 2,000 文、入れ子は 32 段、256 KB
失敗 3 回続いたらそのセッションはオフ。変えたものは元に戻ります

ファイルは、何かが動く前に丸ごと検査されます。見るのは、形と、call がちゃんと存在してグラフに開かれている操作を指しているか、必須の引数、それに {"var"} と {"event"} です。問題のあるファイルはまったく動かず、問題は Mods 画面に並びます。

グラフどうしがぶつかったら

同じものを変える 2 つのグラフは、どちらも書き込んで、あとに書いたほうが残ります。ただ、なぜ自分の変更が効かないのか分からない、ということにはなりません。

  • ログに両方の名前が 1 度だけ出ます。X と Y が両方 cars/car_3 (2) active を変えています。あとに書いた Y の値が残ります。
  • その Mod の Graphs タブに Also changed の行が出ます(どのメンバーを、どのグラフと、が分かります)。
  • 取り消しは、自分が書いた値だけを戻します。あとから誰かが書き換えていたら、その値には触らずログに残します。停止したときに出る件数も、実際に戻した数です(1 つも戻さなければ件数は付きません)。

overrides の行とグラフが同じメンバーを触ったときも同じです。どちらも同じ台帳を通るので、パスの書き方が違っていても互いに気づきます。

コンテンツの方針

グラフに入っているのは、人が書いたもの(イベント、手順、オブジェクトを見つけるための名前)です。コンテンツの方針のとおり、手で作ったものは構いませんが、ゲームのデータをそのまま写したものはいけません。

Clone this wiki locally