【ティラノスクリプト】改ページクリック待ちごとに既読にするプラグインの作り方

2024/07/27追記:
現在バグるので更新記事を書きました。【ティラノスクリプト】テキスト表示ごとに既読にするプラグイン


ティラノスクリプトで改ページクリック待ちごとに既読にするプラグインができた気がするので、おいおい組み込んでゲームを再公開したいと思います。バグってても許して。

せっかくなので、ティラノスクリプトのプラグインが作れなくて悩んでる人のためにどうやって作ったかを書きます。
こういうプラグインの作り方を解説してるサイトが本当に見つからないんですよ……。


  • 本記事はティラノスクリプト Ver510h(2021/9/8 安定版)を使用しています。
  • 動作はティラノスクリプトに同梱されているサンプルゲームで確認しています。
  • 筆者は JavaScript については雑魚なので、用語の説明をしていなかったり、不正確な用語を用いていることがあります。
  • ティラノスクリプト, JavaScript, jQuery などの有識者による訂正コメントやバグ報告は歓迎します! About ページにあるコメントフォームから送ってください!

前書き

今回作成対象とするのは

ティラノスクリプトの既読管理を「改ページクリック待ち」ごとにする

というプラグインです。(通常はラベルごとに既読管理される)
ティラノスクリプトのデフォルトの動作に介入していくプラグインであるため、JavaScript を書いていくことになります。

私がどうやって調査したかや、ゲームをバグらせた過程や、あんまり詳しくない人がハマりそうな間違いも書いているのでかなり長文なんですが、同じように詳しくない方の助けになれると思っています!
結論だけ見たい人はすみませんが STEP5. まで飛ばしてください。

目次

STEP1. 「改ページクリック待ち」を乗っ取る

まずスタート地点としてティラノスクリプトの改ページクリック待ち処理を乗っ取るところから始めたいと思います。

ソースコードを検索していきたいのですが、改ページクリック待ちタグは [p] という非常にシンプルなものなので、タグ名での検索は困難を極めます。
よって data/system/Config.tjs の中にある設定変数名のうち、改ページクリック待ちが関係しそうなもので検索をかけていきます。

たとえば autoSpeed はクリック待ち処理に関係するはずです。

// ◆ オートスピード
//autoSpeed はオート時にどれくらいの早さで進むかをミリ秒設定できる
//autoClickStop は 画面クリックでオートが停止するかどうかを設定。true を指定すると、クリックでオートが停止
;autoSpeed = 1300;

早速ティラノスクリプトのソースコードを VSCode などのお好みのエディタで開いて全文検索します。
なおソースコードはスペースや改行が削除してあって人が読める状態ではないので、お好きな整形ツールで見やすいようにしてください。私は VSCode の拡張機能の Prettier を使いました。

検索結果の内、見るべきなのは tyrano ディレクトリ配下のものです。
検索結果を見ていくと、tyrano/plugins/kag/kag.tag.js に tyrano.plugin.kag.tag.p というオブジェクトが定義されているのを発見できます。

'1'

tyrano.plugin.kag.tag.pstart というメソッドを持っていて、これが改ページクリック待ちの実装に見えます。

確かめるためにティラノスタジオでプレビュー > ゲーム再生 を開き、「デバッグを表示する」にチェックを入れてゲームを再生します。

'2'

デベロッパーツールが開いた状態でゲームが起動するので Source タブを開いて tyrano/plugins/kag/kag.tag.js を開き、tyrano.plugin.kag.tag.pstart メソッド内にブレークポイントを張ってシナリオを実行します。

'3'

実際にやると改ページクリック待ちのタイミングで start が呼ばれるのがわかります。

'4'

では早速改ページクリック待ちを乗っ取りましょう。

https://tyrano.jp/usage/advance/plugin

に書いてある通り、data/others/plugin ディレクトリに今回作りたいプラグインの名称のディレクトリを作り、その中に init.ks と main.js を新規作成します。
※init.ks はこの通りの名前でないとダメですが、main.js は名前はなんでもいいです。 skip.js とか。

今回は適当に skip というプラグインにします。

data/others/plugin/skip/init.ks に下記のように書きます。

[loadjs storage="plugin/skip/main.js"]

[return]

これは main.js を呼び出すというだけの記述です。

処理本体は data/others/plugin/skip/main.js に書きます。

(function ($) {
  (function () {
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    tyrano.plugin.kag.tag.p.start = function () {
      console.log("乗っ取りました!");
    };
  })();
})(jQuery);

「$ とか jQuery とか何?」という方は別途ググってください。これはティラノスクリプト固有の話ではなく jQuery の話なのでググれば解説がたくさん出てきます。
とりあえず動けばいい派の人は「こう書くんだな」と思っておいてください。

あとは data/scenario/first.ks に

[plugin name="skip"]

と書けば完了です。

'5'

さっそくデバッグ表示 ON のままゲームをリロードし、Console タブを見ましょう。

改ページクリック待ちのタイミングで「乗っ取りました!」と表示されます!

'6'

でもそのままシナリオを進めていくと、当然改ページされません。元の動作を完全に上書きしてしまったからです。

これでは困るので、次に改ページクリック待ちしつつ「乗っ取りました!」を表示しましょう。

STEP2. 「改ページクリック待ち」の処理を残しつつ独自の処理を差し込む

元の処理を残しながら独自の処理を行うには、元の処理を保存しておいて呼び出せばいいですね。

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    tyrano.plugin.kag.tag.p.start = function () {
      console.log("乗っ取りました!");
      // デフォルトの start を実行する
      _p.start.call(this);
    };
  })();
})(jQuery);

こんな感じですかね……。
_p.start.call(this)call というのも JavaScript の文法なので知りたい方はググってください。
これは this というオブジェクトとして _p.start() を実行するみたいな意味です。

動作を確認する前にネタバレですが、これは間違いです。
このまま実行すると「乗っ取りました!」が延々と表示される状態となります。

'7'

なぜかというと _ptyrano.plugin.kag.tag.p が同一の物を指している状態だからです。

これを避けるため、ここではデフォルトの tyrano.plugin.kag.tag.p を元にして、start メソッドだけ置き換えた新規オブジェクトを生成するという方向性で修正したいと思います。

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    tyrano.plugin.kag.tag.p = $.extend(true, {}, _p, {
      start: function () {
        console.log("乗っ取りました!");
        // デフォルトの start を実行する
        _p.start.call(this);
      },
    });
  })();
})(jQuery);

$.extend というのは jQuery の文法で、複数のオブジェクトをマージします。
詳しく知りたい方はググってください。(私も初めて使いました)

この結果、_ptyrano.plugin.kag.tag.p は別物になったはずです。動作を確認してみましょう。
「乗っ取りました!」と表示……されません!

'8'

この原因を知るために、STEP1. でやったのと同じく kag.tag.js の tyrano.plugin.kag.tag.pstart メソッド内にブレークポイントを張ってシナリオを実行し、ブレークポイントで止まったら Call Stack を見てください。

start の一つ下に nextOrder と書いてあると思うので、そこをクリックすれば呼び出し元のソースコードを確認できます。

'9'

this.master_tag[tag.name].start(
  $.extend(!0, $.cloneObject(this.master_tag[tag.name].pm), tag.pm)
);

これが呼び出し元のソースコードです。
this.master_tag.p.starttyrano.plugin.kag.tag.p.start が呼ばれているみたいです。

master_tag でソースコードを検索してみると、kag.tag.js ファイルの冒頭付近で、tyrano.plugin.kag.ftag の中に init というメソッドが定義されています。

init: function () {
    for (var order_type in tyrano.plugin.kag.tag) {
      this.master_tag[order_type] = object(tyrano.plugin.kag.tag[order_type]);
      this.master_tag[order_type].kag = this.kag;
    }
  },

object() というのは tyrano/tyrano.js で定義されている関数です。
でも私には完璧な説明が難しいので説明を割愛します。
理解したい方は tyrano/tyrano.js を眺めながら「JavaScript プロトタイプチェーン」とかでググってください。

結論を言うと

this.master_tag.p.__proto__tyrano.plugin.kag.tag.p が等しい

です。

そしてブレークポイントを張ってゲームを起動すると確認できますが、これはプラグインのソースコードを実行する前に実行されます。

これが何を意味するかというと、

  • 実際に実行されるのは this.master_tag.p.__proto__ の参照先の start メソッド
  • 新規オブジェクトとして作った tyrano.plugin.kag.tag.p が勝手に this.master_tag.p.__proto__ の参照先になったりはしない

ということです。

つまり、STEP1. で動作を乗っ取ったときや STEP2. で最初にバグったときは this.master_tag.p.__proto__ の参照先を直接書き換えていたから「乗っ取りました!」が表示できていたのです。

では this.master_tag.p.__proto__ の参照先を変えればいいわけですね!
あとさっきの init メソッドの中には

this.master_tag[order_type].kag = this.kag;

とも書いてありましたから、これも追加しないとダメでしょう。

ところで this って何でしょうか? tyrano.plugin.kag.ftag.init() の中で this って書いてあるんだから、this = tyrano.plugin.kag.ftag ですかね?

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    const p = $.extend(true, {}, _p, {
      start: function () {
        console.log("乗っ取りました!");
        // デフォルトの start を実行する
        _p.start.call(this);
      },
    });

    tyrano.plugin.kag.ftag.master_tag.p = object(p);
    tyrano.plugin.kag.ftag.master_tag.p.kag = tyrano.plugin.kag.ftag.kag;
  })();
})(jQuery);

できました!

ゲームをデバッグ表示 ON で起動し、コンソールを見ながらシナリオを進めてみてください。

'21'

……ダメみたいですね!

エラーが出てしまいました。

なぜかというと、tyrano.plugin.kag.ftag.kagnull だからです。
つまり this = tyrano.plugin.kag.ftag ではありません。

ここで一旦ゲームの起動処理を見ていき、真の this を探しましょう。

tyrano/tyrano.js 内で

TYRANO.init();

と書いてあるのがティラノスクリプトの起動処理です。

'10'

この TYRANO こそティラノスクリプトの本体であり、ゲーム内のあらゆる場所に書いてある thisTYRANO を使って表現できるはずです。(たぶん)

同ファイル内を読み解いていくと、

TYRANO = object(tyrano.core) なので、TYRANO.init() と書くと呼ばれるのは tyrano.core.init() です。
※これが「プロトタイプチェーン」の効果。TYRANO 自身は init メソッドを持っていないが、TYRANO.__proto__ の先まで init メソッドを探しに行き、存在したらそれを実行する仕組みとなっている。

TYRANO.init 内を辿ると this.kag = object(tyrano.plugin.kag) になるので

TYRANO.kag.__proto__ = tyrano.plugin.kag

となります。
次に TYRANO.kag.init()tyrano.plugin.kag.init() なので tyrano/plugins/kag/kag.js 内へ飛び、TYRANO.kag.init_game() 内で

TYRANO.kag.ftag.__proto__ = tyrano.plugin.kag.ftag
TYRANO.kag.ftag.kag = TYRANO.kag

となります。

最後に TYRANO.kag.ftag.init()tyrano.plugin.kag.ftag.init() が呼ばれ、this.master_tag.p = object(tyrano.plugin.kag.tag.p) とあるので

TYRANO.kag.ftag.master_tag.p.__proto__ = tyrano.plugin.kag.tag.p

となりました。

つまり ④ より、さっき見ていた this.master_tag.pthis とは TYRANO.kag.ftag です。

ということで、③ と ④ の関係より、プラグインを下記のように修正します。

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    const p = $.extend(true, {}, _p, {
      start: function () {
        console.log("乗っ取りました!");
        // デフォルトの start を実行する
        _p.start.call(this);
      },
    });

    TYRANO.kag.ftag.master_tag.p = object(p);
    TYRANO.kag.ftag.master_tag.p.kag = TYRANO.kag;
  })();
})(jQuery);

デバッグ表示 ON でゲームを起動すれば、今度こそ「乗っ取りました!」が表示されるのに加えて改ページクリック待ちも実行されます。

'11'

STEP3. ラベル記録の実行方法を探す

次にティラノスクリプトで既読管理のために用いられるラベル記録の実行方法を探します。

まず最初に data/system/Config.tjs の autoRecordLabel を true にしてください。これがないと既読管理が動きません。

'12'

ついでにここに既読管理のためのラベル記録の説明が書いてあります。
それによると sf.record という変数に既読を記録するので sf.record でソースコードを検索します。

すると tyrano/plugins/kag/kag.tag.js に tyrano.plugin.kag.tag.label というオブジェクトが定義されており、start というメソッド内に下記のような実装があります。

this.kag.variable.sf.record &&
  (this.kag.variable.sf.record[sf_label]
    ? (this.kag.stat.already_read = !0)
    : (this.kag.stat.already_read = !1));

ちょっと独特の書き方ですが Config.tjs に書いてあるラベル記録の説明と一致するので、これがラベル記録の処理になります。

これを改ページクリック待ちのタイミングで呼ばなければいけないので、呼び出し方を探します。
デバッグ表示 ON でゲームを起動し、tyrano.plugin.kag.tag.label.start 内にブレークポイントを張って Call Stack を見ます。

'13'

すると呼び出し元は同ファイル内の tyrano.plugin.kag.ftag.nextOrderWithLabel で、

nextOrderWithLabel: function (label_name, scenario_file) {
    this.kag.stat.is_strong_stop = !1;
    if (label_name) {
      -1 != label_name.indexOf("*") &&
        (label_name = label_name.substr(1, label_name.length));
      this.kag.ftag.startTag("label", {
        label_name: label_name,
        nextorder: "false",
      });
    }

this.kag.ftag.startTag という関数で、引数にラベル名と nextorder: "false" を渡して呼ぶことがわかりました。

STEP2. で調べた結果から TYRANO を使って表現すると、TYRANO.kag.ftag.startTag を呼べばいいです。

試しにプラグインから読んでみましょう。

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    const p = $.extend(true, {}, _p, {
      start: function () {
        console.log("乗っ取りました!");
        TYRANO.kag.ftag.startTag("label", {
          label_name: "skip",
          nextorder: "false",
        });
        // デフォルトの start を実行する
        _p.start.call(this);
      },
    });

    TYRANO.kag.ftag.master_tag.p = object(p);
    TYRANO.kag.ftag.master_tag.p.kag = TYRANO.kag;
  })();
})(jQuery);

これでティラノスタジオでゲームを起動し、起動状態でティラノスタジオのメニューの
プレビュー>セーブデータ
から「セーブデータをすべて削除する」を押下してセーブデータを消します。

'14'

次に一度ゲームをリロードしてから
プレビュー>変数ウォッチ
を見て、「sf.record」を追加します。

'15'

ゲームを進め、コンソールに「乗っ取りました!」が2回表示されたタイミングで sf.record の値を確認すると、“trail_scene1_skip”: 1 のように保存されていると思います。

'16'

これでラベル記録を呼ぶことができました。

STEP4. 改ページクリック待ちごとに一意のラベルを生成する

改ページクリック待ちごとに既読とするには、改ページクリック待ちごとにシナリオファイル内で一意のラベルを生成して記録する必要があります。

そのための助けになりそうな値がデバッグ表示 ON でゲーム実行した際のコンソールにずっと出ています。

**:21 line:46
kag.js:1 {line: 46, name: "p", pm: {…}, val: "", ks_file: "scene1.ks"}

この書き出しがある箇所から Call Stack で呼び出し元を見ると、tyrano/plugins/kag/kag.tag.js 内に

this.kag.log("**:" + this.current_order_index + " line:" + tag.line);

このような記述を発見できます。

'18'

tag.line はシナリオの行数のようですが、this.current_order_index は?

幸いすぐ近くに答えがあります。

var tag = $.cloneObject(this.array_tag[this.current_order_index]);

試しにこの行のすぐ下に console.log(this.array_tag); と書いてゲームをリロードしてみてください。

'19'

'20'

this.array_tag は、ティラノスクリプトがシナリオファイルを解析し、実行する命令を順番に配列に詰めた物です。
this.current_order_index とは現在実行している命令のインデックスを示します。

つまり this.current_order_index を使えば、シナリオファイル内で間違いなく一意のラベルを生成することができます。

例によって TYRANO を使って表現すると、TYRANO.kag.ftag.current_order_index です。

STEP5. プラグイン実装

いよいよ完成版のプラグインを作ります。

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    const p = $.extend(true, {}, _p, {
      start: function () {
        TYRANO.kag.ftag.startTag("label", {
          label_name: "skip" + TYRANO.kag.ftag.current_order_index,
          nextorder: "false",
        });
        // デフォルトの start を実行する
        _p.start.call(this);
      },
    });

    TYRANO.kag.ftag.master_tag.p = object(p);
    TYRANO.kag.ftag.master_tag.p.kag = TYRANO.kag;
  })();
})(jQuery);

これで改ページクリック待ちごとに既読になります。動作を確かめてみてください。

ただし下記の制限があります。

  • シナリオ内に自動生成と被るラベル名を書いてはいけません。今回の実装なら「skip」で始まるラベルを書かなければ大丈夫です。
  • 改ページクリック待ちタグを組み込んだマクロを定義した場合、うまく動きません。
    • マクロが呼ばれると一時的に TYRANO.kag.ftag.current_order_index はマクロが記述されたファイル(大抵 first.ks)内での命令順に代わってしまうため。
  • シナリオを書き替えた場合も正しく動かなくなります。シナリオを書き替えたらセーブデータを全削除して既読管理状態をリセットするようにしてください。

ちなみに私のクレジット表記無しでゲームに組み込んでいただいても構いませんが、私は動作を保証しません。自己責任でお願いします。
(でもバグってたら私にも教えてほしいです!)

なお注意点としては、_p.start.call(this) を最後に呼ぶようにしないと、一意なラベル生成のために使っている TYRANO.kag.ftag.current_order_index の値がスキップ中かどうでないかによって変わってしまいバグります。

なぜ値が変わるかというと、スキップ中はクリック待ちを行う必要が無いため、_p.start.call(this) 内で次の処理に移行してしまうからです。
動きを見てみたい人はプラグインの中身を

(function ($) {
  (function () {
    // デフォルトの [p] を変数に保存する
    const _p = tyrano.plugin.kag.tag.p;
    // デフォルトの [p] の動作(start)を独自の関数で上書きする
    // [p] の定義が持っていた start 以外の定義は引き継ぐ
    const p = $.extend(true, {}, _p, {
      start: function () {
        TYRANO.kag.ftag.startTag("label", {
          label_name: "skip" + TYRANO.kag.ftag.current_order_index,
          nextorder: "false",
        });
        console.log("[p]実行前" + TYRANO.kag.ftag.current_order_index);
        // デフォルトの start を実行する
        _p.start.call(this);
        console.log("[p]実行後" + TYRANO.kag.ftag.current_order_index);
      },
    });

    TYRANO.kag.ftag.master_tag.p = object(p);
    TYRANO.kag.ftag.master_tag.p.kag = TYRANO.kag;
  })();
})(jQuery);

としてゲームをデバッグ表示 ON で起動し、手動クリックでシナリオを進めたり、スキップしたりしてみてください。
手動クリックのときは [p] 実行前後で同じ番号がコンソールに表示されるのに、スキップ中は異なる番号が表示されているし、[p] の次に処理が移行しているのがわかります。

なお、もし今後ティラノスクリプトの実装が変更され、スキップ中やオートモード中にそもそもクリック待ち処理を呼ばなくなったらこのプラグインは動かなくなります。
かなり基幹の実装なので大丈夫だと思ってますが、それが嫌な場合はどんなときでも実行されそうな処理内で既読にするといいと思います。


以上で実装完了です。
これを応用すればきっとティラノスクリプトを好きに拡張できると思います!
よいゲーム制作ライフを送ってください!