ChatGPTの手を借りつつ、超簡単なWindowsネイティブアプリをRustでつくった

最近、おそらくは常駐させているなんかのアプリの影響で、急にウィンドウのフォーカスを奪われることがある。例えば、普通にVSCodeで何かを書いてる時、急にフォーカスが飛ぶと普通にびっくりするし、何度も繰り返されるとイライラする。

直接的な原因になりそうなアプリが思いつくわけでもなかったので、どうにかしてフォーカスを奪ってくるアプリを知る必要があると思った。

こういうニッチなケースに関しては、サクッとコードを書くのが良いとされているので、そういうコンソールアプリを作ろう!と思い立った。

WindowsのAPIをバリバリに叩くアプリだし、しかも今回の場合はフック先の関数とかも書かないといけなさそうなので気が重くなりそうなところだけど、今はChatGPTという対話型最高アプリが存在するので腰を上げることができた。

ChatGPTは枯れつつあるプログラミングの知識についてはかなり良いものを持っており、今回のような例に関してはまさに持って来いの話題なのでは?と考え、今回はこいつにスタートダッシュを共にしてもらい、アプリを完成させた。

github.com

以下のような感じで、フォーカスがあたっているアプリが変更されたらそれにフックしてアプリの概要をプリントする、という簡単なもの。

アプリの様子

リポジトリ用のディレクトリが作られ、最終更新が発生したタイミングまで全部で35分くらいしか使っておらず、とても素晴らしかったので良かった点を記事として残しておく。

前提として、自分は今回使われるスタックについてはプロレベルではないにせよすべて一度以上は使ったことがあり、かつプログラミングについての全般は慣れ親しんでいる。プログラミングができない初心者がChatGPTを使ってアプリを作り上げたという記事ではないことに注意。

全体像は 会話全文 を読んでもらえば良いが、良かった部分をいくつか抜き出す。

会話

まずは要件を伝えてみると、C#のコードを返してきた。C#で触るWin32のAPIやラッパーはかなり好感触なんだけど、C#を触るために最適な環境であろう Visual Studio を触りたくなさすぎる。純粋にIDEとしては動作が重すぎるし、依存の在り方が複雑だし、正直なところ、触るだけで気が滅入ってしまう類のプロダクトだと思う。

要件を伝えると、C#のコードが返ってきた

ので、今回はポータブル性に優れており、かつ現代的な構文やエコシステムを持ちながらも、C/C++と同レベルのレイヤーのコードを各ことができる、僕のイチオシ言語 Rust で進めてもらうことにした。すると、一発目からなんだか動きそうなコードを出してきた。

いい感じなんだけど、個人的には winapi よりも windows を使う方が好みなので、こちらを使うように変更する指示を出してみる。

(この下部に src/main.rs のコードあり)

現在のChatGPTのGPT-4のモデルは2023年6月までで知識が止まっているため、古いバージョンのクレートを提示してくるが、これは自分の方で新しいものになるように吸収していく。提示されたソースコードをコピペすると、まあ当然のようにビルドエラーが出る。インポートエラーは正しいものになるように解決したが、どうも GetWindowTextW の使い方が間違っている。そもそも、引数の数が違っており、ChatGPTの提示では3つ渡されているが、実際の引数は2つを期待している様子だった。

この場合、関数のシグネチャを直接教えてやると、それに従ったコードを提示するようになる。

この時点で充分動くコードができあがるが、実際に動かしてみたところ、タイトルが表示されるだけでは情報が足りないことに気づく。ここで要件を追加してみる。

なんか色々言ってるが、WindowsのAPIをうまく使ったコードが生成された。これを必要な部分だけコピペし、動かしてみる。VSCode側ではGitHub Copilotも動いたりしていい感じにできあがっていくが、一点だけ良くない点が生まれた。この時点で、既に指示から逸脱して自分流の書き方をいくつかしていたため、以下のように現在地点を伝えるつもりでソースコード全文を貼り付ける。

いい感じの解決策を提示してくれた。PROCESS_NAME_WIN320 に直されたのは情報が古い問題によるもので、これを治したければまた正しいシグネチャを教えることでどうにかなりそうだが、あえてそこまで教えてやる必要もなく、こちらで修正して完了させた。

また、リポジトリの名前も考えてもらった。自分はあまり気の利いた名前を考えるのが得意じゃないので、頻繁にChatGPTに頼っている。

今回は愚直な名前が返ってきた。いい感じ。GitHubにリポジトリをpushしておいて、作業終了。あとはこれを常駐させて、実際に犯人特定の手がかりになればよいな~というところ。

最終的な成果物は以下に存在する。(再掲)

github.com


こんな感じで、知ってることであっても大体最初のとっかかりはChatGPTに書いてもらうことが多い。これまではGoogleで検索し、それっぽい記事のそれっぽいコードをコピペしていたが、検索結果のノイズが激しくなってきたことと、GPT-4の精度の高さを見込んで最初からChatGPTを使うようになった。

特に今回のような「Windowsの特定のイベントフック( SetWinEventHook あたりのやつ )をRustで書く」というニッチなケースに対してもいい感じに、しかも日本語で提示してきた。以下に提示した event_callback 関数のインターフェイスと main 関数はほとんどコピペで作られている。event_callback の各引数や、unsafe extern "system" などのキーワードはある程度unsafe Rustの、しかもWindowsを相手取っていないとサッと出てこないコードだろうし、main 関数に至っては最初から最後まで一文字すら変更していない。実際、main 関数の書き方は自分が一番億劫に感じている部分でもあるので、これが一発で出てきてくれたのはとてもありがたいことだった。

unsafe extern "system" fn event_callback(
    _h_win_event_hook: HWINEVENTHOOK,
    _event: u32,
    hwnd: HWND,
    _id_object: i32,
    _id_child: i32,
    _id_event_thread: u32,
    _dwms_event_time: u32,
) {
    // snip
}

fn main() -> Result<()> {
    unsafe {
        let event_hook = SetWinEventHook(
            EVENT_SYSTEM_FOREGROUND,
            EVENT_SYSTEM_FOREGROUND,
            None,
            Some(event_callback),
            0,
            0,
            WINEVENT_OUTOFCONTEXT,
        );

        let mut msg: MSG = MSG::default();
        while GetMessageW(&mut msg, HWND(0), 0, 0).into() {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }

        UnhookWinEvent(event_hook);
    }
    Ok(())
}

WindowsのAPIを相手にするプログラミングは苦痛が多いことで知られているし、Rustの関数をWindowsが叩けるように渡す行為はさらに苦しいことが多いが、(もちろん要件の簡単さもあれど)テンプレートとしては最もすばらしいものを提示してくれたおかげで、実際に書きたいドメインロジック的な部分を超高速で仕上げることができた。とても嬉しい。

映画館で音MADを見た!!!!

よっしゃーーーーー

「映画館で見る音MAD」という名前の催しに参加しました!自分はセットリストとかを提出したわけではなく、単にみんなが選んだ音MADを見るだけ、という超気楽なポジション。最高やね。

まずは誘われたきっかけから。

2023年の1月頃に流れてきたきっかんさんの(おそらく第一回?)の感想記事を見かけた。

note.com

第一回開催の話は人伝に聞いたりしていたため、なんとなく良さそうなイベントだな~と受け取っていた。やはりというか、大変素晴らしいイベントだった様子で、大変羨ましい感じ。

で、それを共有したツイートがこれ↓

なんと、これがきっかけになってきっかんさんにお誘いいただいたらしい。第三回で追加人員を募っていたようで、その枠に入れていただけた様子。嬉しい限りでございます。本当にありがとう。

以下感想とか。

映画館は映像メディアを見るにはとても適した場所で、音MADも例外ではなかった。良い音と良い画にのみ集中できる環境に自らを置けるというのは大変素晴らしく、音MADの魅力を受け取る場所としてかなり素晴らしかった。

その音MADの魅力も、設備のおかげでかなりのブーストが掛かる。左右反転も映画館だとデカい。映像の奥行きも、音のダイナミクスレンジも、きれいな色の綺麗さも、何もかもがデカい。

あと、とても新鮮だった。通常、音MADはニコニコ動画やYouTube、Bilibiliなどを介してPC用モニターやスマホから見ることが多いので、そうでない環境で見るというだけで普段と違う感覚を覚えた。(類似: nerdtronics、10選オフ、知り合いの部屋で見る音MAD など)

で、皆さんのセットリストが本当によかった。単純に様々な音MADを見るための場としてもとても良かったんだけど、各人のベクトルはそれぞれ違っていて、音MADを見るときの心持ちを伺うことができたと思う。ぞれぞれ別個の音MADのであっても、それが15分も流れていれば一つのコンテキストとして見せることができる。見たことのある音MADも少し違う趣を持って受け取ることができた。

今回、物語/空気感を表すという点から、ひとつ本当に素晴らしいセットリストがあった。(ちょっと話を脱線させて……)普段の自分は、音MADに感じられるプリミティブなグルーヴ感を主軸に考えて行動している。例えば nerdtronics 2 とか 2.5 とかでやったようなDJは、そのことを最も主軸に考えていて、普段作ろうとしているものもそのポリシーから外れることはない。(話を戻して……)そんな中、まるで100点みたいに音MADで物語を見せてくれて、普段の自分との解釈の違い方にかなり驚いてしまった。基礎としてのビートの聞こえが本当に素晴らしかったこと、そもそも物語が綺麗で心地よかったことも起因して大きく心を動かされ、結構泣いてしまった。すごく気軽に音MADを見に来たのに、ここまで素晴らしい体験ができたので本当に嬉しかった。

(一方、あまりハードルが上がっても良くないよねーという話も聞いたので、塩梅は難しいだろうなと思った。みんな色々と上手なので、繰り返せば自然と洗練されていくものだし。もしできそうだったら、僕も気軽にセットリストを作って投げちゃいたいね!!)

素晴らしい場所のセッティングとか、このイベントの空気感とか、セットリストが再生される順番みたいなものもまさに素晴らしい場を組み上げるための流れのはずで、これを作り上げた主催のR.M.さんをはじめ、皆様に大きな感謝。

将来についての話とかも聞けて、今後のことにも超期待。また行けるなら行きたい。


余談!終わったあとの打ち上げで、好きな音楽レーベルが近しい方を見つけた。こんなことって中々ないので、本当に嬉しかった。あまり交流しない方々と遊んだりできるのって、それだけで嬉しいことだよなー

まだ辞書を引いていない単語がたくさんありますね

ツイートをちゃんと記事にしてみようのコーナー👏👏 以下のツイートのちゃんとした記事版です


人のコードを読んでるときにはじめて出会った英単語って結構多くて、それらの単語をその周辺の豊富なコンテキストからなんとなくで理解した上になんとなくのまま自分も使ったりしてしまうから、人にその言葉の意味を聞かれても即答できなくて、調べてみて超納得する……みたいなことがとてもよくある。

例えば primitive の意味は今さっき初めて辞書引いたけど、自分のニュアンスとかなり近い結果が返ってきた。

そういった単語は人の技術記事でもカタカナ英語のまま使用されることが多くて、その場合は「日本語の」豊富なコンテキストになるので余計に辞書を引かなくてもなんとかなってしまう。

他にも context(さっきコンテキストっていうカタカナ語を使ったね) とか seek とか parse とか schema とか usecase とか browse とか server とか、ソフトウェア関連でよく出るような単語かつ日本語で書く時にカタカナになってしまう単語たちは多く存在している。なんならもっとたくさんあるし、ここで挙げたものより、もっと納得感のある英単語もあるはず。(サッと脳内のindexから引けなかったわけだけども。)

そもそも日本語にする上で最も近い概念がなく、明治期の日本の方々がやっていたような翻訳語を作らなければならない場面なのにそれができていないからこうなっているんじゃないか?とか思って検索したら以下のような記事が出てきた。

明治期のように、外来語を表す新たな翻訳語を作るというのはどうでしょうか - ことばの疑問 - ことば研究館

一方、現代もこれに倣って、新規の外来語に対して、次々に訳語を新造すればいいでしょうか。現代社会の外来語に接する人々の層の厚さや広さは、知識階層に限られていた明治期とは比較にならず大規模です。一般社会への情報伝播も、マスメディアを通じて一気に広まるなど、事情は大いに異なります。

はーんなるほど、確かに僕はこれらの単語の言い換えとして適切な日本語を見つける/作成されるより前にこの英単語たちと触れているし、そういった単語はみんな僕と同じようにニュアンスで理解しているのだろうな〜。翻訳語を作ろうとしても、それが広まる前にみんながその英単語に慣れてしまった、という状況なのかもしれない。

これは確かによく理解できる。ソフトウェアエンジニアなんかは英語圏の人が書いた文章や単語を見る機会はかなり多いだろうし、知らない分野の日本語の説明なんて最初から当てにしていない節もあるだろう。そのような状況において出てきた知らない単語たちを調べるより、周りのコンテキストから判断して意味を類推するほうが幾分楽なのだろう。また、知らない分野というのは専門性が高くなっており、適する日本語が見つかるかどうかの確証も持てない。しかも、その分野を勉強している間はその概念を英単語としてよく聞くわけで、そうなったらその概念の母親は英語になってしまう。


ここまで書いていて、「じゃあ、どんな外の言葉に対しても*1 "意味を持つ翻訳語" を持っているように見える中国語はどうなんだろう」とか思ったんだけど、そもそもすべての単語が漢字の組み合わせによってできているから造語を作りやすいのだろうし、日本語より楽なのだろうな〜と思った。日本語だと中国語よりも漢字単体の覚える数は少ないだろうし、その分日本語はひらがなとカタカナでカバーしているのだ、という仮説。

ちょっとだけ調べてみたが、中国語における外来語の多様性を見ると、どうやら中国語でも読み方に当てはめた「音訳」という概念はがあるらしく、また外来語に関しては音訳が最も割合高く存在している様子。ただ、分野ごとに見ると、物理、電気、武器、機器みたいなカテゴリでは意訳が多いらしい。

ソフトウェアに関して言えば、技術的な操作やUI上の指示とかオプション、そんなに新しくない技術用語については意訳が使われることが多いらしい。そんなに新しくない技術用語というところは結構面白くて、例としては以下のような対応がある(ChatGPTのGPT-4に聞いてみた)。

  • ブラウザ (ブラウザー) / 浏览器 (liú lǎn qì)
    • 「浏览」は「ブラウズする、閲覧する」を意味し、「器」は「ツール、器具」を意味します。
  • サーバー / 服务器 (fú wù qì)
    • 「服务」は「サービスする」を意味し、「器」は「ツール、器具」を意味します。
  • クライアント / 客户端 (kè hù duān)
    • 「客户」は「クライアント、顧客」を意味し、「端」は「端末、側」を意味します。
  • データベース / 数据库 (shù jù kù)
    • 「数据」は「データ」を意味し、「库」は「倉庫、データベース」を意味します。
  • インターフェース / 界面 (jiè miàn)
    • 「界」は「境界」を、「面」は「面、界面」を意味します。
  • オペレーティングシステム (OS) / 操作系统 (cāo zuò xì tǒng)
    • 「操作」は「オペレーション、操作」を、「系统」は「システム」を意味します。
  • プログラミング / 编程 (biān chéng)
    • 「编」は「編む、編集する」を、「程」は「プログラム、コース」を意味します。

これらはまさに自分が英語のまま捉えている単語なのだけど、中国語ではこれらをちゃんと国内の意味に沿った漢字を宛がえている。おもろい。

また、先程自分が挙げた仮説のようなことも喋っていた。

  1. 言語特性と表現の豊かさ

中国語は漢字を使用しており、各漢字には独自の意味があります。このため、新しい概念や技術が導入される際、既存の漢字を組み合わせて新しい用語を作り出すことができ、その結果として意訳された単語が自然に形成されます。これにより、単語自体がその機能や性質を直接的に反映することが可能になります。

ChatGPTとの対話

また、もう少し調べるとこのような文献*2を発見した。見るに、日本と比較して長期間、外国から単語を仕入れ、何らか訳してきたとのこと。シルクロードとかもあるし、その影響は大きかったのだろうが、中国国内ではそれらは外来語であるという意識も薄いらしい。おそらくは日本語よりも外来語を意訳することについての抵抗が少ないのだろう。

さらに調べると、この記事の冒頭では以下のような記載もあり、やはり日本語と比べるとカタカナ語みたいな、いかにもな外来語は多くなさそうに見える。

中国の日本語学習者は、外来語が多いことに戸惑うことがよくある。

このあたりは言語や国柄の特性の違いによるバランス感を感じる。この手の単語に意訳が多そうな中国語はちょっとうらやましく感じるが、日本語のカタカナで表した単語もそれはそれでコンテキストを損なわずに伝えることができるので嬉しいのだろうなとも思ったりする。深掘りすればもっと色々なものが要因になっていそうなトピックだが、別に言語学者になるほどの熱はないのでこの辺で手を引いておきたい。


ニュアンスで言葉を理解しているときに困るのが(最初に書いた通り)人に説明を求められるような状況で、言語化しなくてもどうにかなっていた部分を急に言語化しなければならなくなって超大変になる。

こういう状態はべつに英単語に限った話ではなくて日本語の単語にもあると思うんだけど、みんなが理解しているような言葉であっても、その言葉の説明を行うのって本当に難しいなと思う。し、その上で辞書を引くとおおよそ納得できる、筋が良さそうな説明が出てきてくれるのでとてもすごいと思う。超グッドワーク。

*1:偏見

*2: 中国語の外来語形成のプロセスと現状 (クリックするとすぐpdfのダウンロードが始まるので注意)

Minecraft 1.16.1 でのエンダードラゴンの挙動をまとめた

Minecraft 1.16.1 でのエンダードラゴンの挙動の詳細を調査した。

最近Minecraft 1.16.1のRTAにハマっている。エンダードラゴン戦をすばやく終わらせることはタイム短縮に大きくつながる行動だが、エンダードラゴンの挙動は思った以上に複雑であり、これらの状態や条件を詳しく知る必要が出てきた。今回は、Minecraftの内部実装を確認し、エンダードラゴンが実際にどのように動き、何に反応して行動を変えているのか?を調査した。

前提

コードリーディングにおいて、Fabricプロジェクトの yarn 1.16.1+build.21 を使用してマップ済み(≒難読化解除済み)のコードを生成してもらい、それを元に情報を調査した。

また、一部ステート遷移の確認には、より新しいバージョンである yarn 1.20+build.1 を使用して生成したコードを使用した。これについては、コード上に影響のある変化がないことを確認した上で調査を進めている。

動的な確認を行うために 1.16.1 向けのMODを作成している。その際のソースコードなどは下記リンクから確認できる。

なにか間違いがありそうであればXのDMかコメント欄まで。

用語集

広く使われている/使われていないに依らず、この記事内で使用されている特殊な用語の一覧。

Perch

(X: 0, Z: 0) にある最も高い位置の任意のブロックを指す。通常、エンドの出口ポータルの中心にある柱の頂点がこれに該当する。*1

エンドの出口ポータル。通常、中心の柱の頂点が (X: 0, Z: 0) において最も高いブロックとなる。

パスノード

エンダードラゴンの経路として設定される、24個の座標を指す。平面図に描画すると以下のように規則的に点在している。

高度については、基本的には同じ (X, Z) 内で最も高い場所に位置するブロック + (5 または 10) が採用される。特に、(X: 40, Z: 0) の円周にあるパスノードのうちいくつかは黒曜石の柱の上空に位置することがある。

各パスノードの位置。(X: 0, Z: 0) の位置に岩盤の柱(Perch)が存在する。

フェーズ

エンダードラゴンの挙動を区切るための状態や段階を表したもの。エンダードラゴンの挙動は内部的には11個のフェーズが存在しており、それぞれ別々のクラスで実装されている。今回は、主にこれらのクラスを読み解くことでエンダードラゴンの挙動の詳細を調査した。

ステート遷移図

まず、自分自身が理解できるようになるためにステート遷移図を書いた。書いたのだが、あまりにも見辛い図ができてしまったため参考程度に見ていただくとよいと思う。

別タブで開く

stateDiagram-v2
    [*] --> Hover : エンティティ生成時
    state 生きてる {
        state after_holding_pattern <<choice>>
        state after_sitting_scanning <<choice>>
        state after_sitting_flaming <<choice>>
        Hover --> HoldingPattern : エンティティ生成直後
        HoldingPattern --> after_holding_pattern : 目的地から遠すぎ/近すぎるとき…
        after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率で
        after_holding_pattern --> StrafePlayer : 1 / (プレイヤーとPerchとの距離) の確率で\nまたは、1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
        StrafePlayer --> HoldingPattern : プレイヤーがいないとき
        LandingApproach --> Landing : 目的地から遠すぎ/近すぎる場合
        Landing --> SittingScanning : Perchの直上に来たら
        SittingScanning --> after_sitting_scanning : 近くにプレイヤーがいればその方を向く
        after_sitting_scanning --> SittingAttacking : 近くにプレイヤーがいてすこし経過している場合に
        after_sitting_scanning --> Takeoff : しばらく経っても周りに誰もいない場合に
        after_sitting_scanning --> ChargingPlayer : しばらく経過した時、そこそこの範囲にプレイヤーがいれば
        SittingAttacking --> SittingFlaming : 少ししたら
        SittingFlaming --> after_sitting_flaming : しばらくしてから…
        after_sitting_flaming --> SittingScanning : 4回未満の場合に
        after_sitting_flaming --> Takeoff : 4回以上の場合に
        Takeoff --> HoldingPattern : パス更新後、Perchに近くなったら
        ChargingPlayer --> HoldingPattern : 少ししたら
    }
    生きてる --> Dying : HPが0になったら
    Dying --> [*] : 死亡演出が終わったら

また、これを作る前に捉えられる情報を殆ど記載しているバージョンのステート遷移図を作成した。これをもとに情報を落としたバージョンが上記の図。もし、情報を確認したかったり、いくらでも拡大できるような環境なのであれば以下を見ても良いかも?

ここから確認できる

別タブで開く

stateDiagram-v2
    [*] --> Hover : エンティティ生成時に PhaseManager でセット
    state 生きてる {
        state after_holding_pattern <<choice>>
        state after_sitting_scanning <<choice>>
        state after_sitting_flaming <<choice>>
        Hover --> HoldingPattern : エンティティ生成直後の処理内部の EnderDragonEntity#createDragon() でセット
        HoldingPattern --> after_holding_pattern : 目的地から遠い(150より遠い)または近い(10より近い)とき…
        after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率でセット
        after_holding_pattern --> StrafePlayer : 1 / int((プレイヤーとerchの距離)の2乗を512で割った数 + 2.0) の確率で\nまたは 1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
        StrafePlayer --> HoldingPattern : 距離が64より近ければ、現在のpathを移動を完了してから攻撃して、終了後にセット\n-> 目的地から遠い(150より遠い) または 近い(10より近い) 場合はパスの更新が発生\n-> 5tick以上プレイヤーを見たら火の玉で攻撃する\n\nまたはプレイヤーがいなければセット
        LandingApproach --> Landing : 目的地から遠い(150より遠い) または 近い(10より近い) 場合にパスを更新、パスの移動が完了した時にセット
        Landing --> SittingScanning : ターゲット(Perch)までの距離が1未満の時にセット
        SittingScanning --> after_sitting_scanning : 距離20までにプレイヤーがいれば一番近いプレイヤーの方を向く
        after_sitting_scanning --> SittingAttacking : 距離20までにプレイヤーがいて25tick以上経過している場合にセット
        after_sitting_scanning --> Takeoff : 100tick経過後、誰もいない場合にセット
        after_sitting_scanning --> ChargingPlayer : 100tick経過後、距離20より離れてて距離150までにプレイヤーがいればセット そのままプレイヤーに向かってくる
        SittingAttacking --> SittingFlaming : 40tick経過後にセット
        SittingFlaming --> after_sitting_flaming : 200tick経過後…
        after_sitting_flaming --> SittingScanning : 連続回数が4回未満の場合にセット
        after_sitting_flaming --> Takeoff : 連続回数が4回以上の場合にセット
        Takeoff --> HoldingPattern : パスを更新して移動、Perchから距離10以内になったらセット
        ChargingPlayer --> HoldingPattern : 最長10tickの間、プレイヤーに突っ込んでくる\nプレイヤーから遠い(150より遠い) または 近い(10より近い)場合はtickが短くなる
    }
    生きてる --> Dying : HPが0になった時にセット (内部的にはDYING中はHPが1になる)
    Dying --> [*] : Perchから10より近い または 150より遠い時にセット\n(HPも0になってエンティティが消滅する)

RTA走者向け

RTA走者向けには以下で充分だと思う。スポーンしてからベッド爆破で倒すまで。

別タブで開く

stateDiagram-v2
    [*] --> HoldingPattern : エンティティ生成直後
    HoldingPattern --> after_holding_pattern : 目的地から遠い(150より遠い)または近い(10より近い)とき…
    after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率で
    after_holding_pattern --> StrafePlayer : 1 / int((プレイヤーとPerchの距離)の2乗を512で割った数 + 2.0) の確率で\nまたは 1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
    StrafePlayer --> HoldingPattern : プレイヤーがいないとき\nまたは 距離が64より近ければ、現在のpathを移動を完了して攻撃してからセット\n-> 目的地から遠い(150より遠い) または 近い(10より近い) 場合はパスの更新が発生\n-> 5tick以上プレイヤーを見たら火の玉で攻撃する
    LandingApproach --> Landing : 目的地から遠い(150より遠い) または 近い(10より近い) 場合にパスを更新\nパスの移動が完了した時にセット
    Landing --> Dying : Perchの直上に来たらベッド爆破で倒す
    Dying --> [*] : 死亡演出が終わったら

エンダードラゴンの挙動の詳細について

エンダードラゴンの挙動を流れで追っていく。各見出しは内部で設定されているフェーズの名前。

スポーン直後

おおよそ初期状態に近い状態。基本的には HoldingPattern のみとなる。

Hover

エンティティ生成直後のバニラではあまり見ない状態。バニラのサバイバルで通常通り生成される場合、この後の処理によって同tick内に HoldingPattern に移行する。

/summon minecraft:ender_dragon などによって生成されたエンダードラゴンはこの状態から始まり、常に同じようなY高度を保ちながらぐるぐると飛び続ける。

HoldingPattern

バニラで最初に見られる状態はこれ。最も近い位置に存在するパスノードと周辺のパスノード(詳細は後述)を選択し、そのパスノード上部(Y: +0~20)をターゲットとして向かう。ターゲットへの移動が終了した場合、同じロジックで次のターゲットを発見し、以後同じように移動を繰り返す。

エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い 場合に以下のようなステート移行のチャンスを得る。

パスノード選択の詳細

パスノードの選択についてもう少し詳細に記載する。エンダードラゴンは (X: 0, Y: 128, Z: 0) にスポーンし、最も近い位置に存在するパスノードを取得する。初期の状況的には、東(16)か西(12)のどちらか高い方の黒曜石の柱の上にあるパスノードが選択されることになる。その後、抽選が行われ、1/8の確率で +7 、それ以外の場合は -1 のパスノードを選択する。エンダードラゴンは最初に選択したパスノードを経由したあと、2つ目のパスノードの上空(Y: +0~20)をターゲットとして向かう。(初期状態ではより高いところにいるため、ゆっくり降下してパスノード上空(Y: +0~20)に向かうこととなる。)

向かっている最中、先程記載した条件 (エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い) を達成し、別のフェーズに移行できなかった場合に再度パスノードとターゲットの選択の動きを繰り返す。

1/8 の抽選に当選する度にテーブルが変動するが、詳細は以下のコードを参照すること。

ここから確認できる

if (this.dragon.getRandom().nextInt(8) == 0) {
    this.shouldFindNewPath = !this.shouldFindNewPath;
    i += 6;
}
i = this.shouldFindNewPath ? ++i : --i;

火の玉攻撃

火の玉攻撃をするための状態。

StrafePlayer

こちらにドラゴンの火の玉を吐き、攻撃してくる状態。HoldingPattern から移行してくる時に事前にターゲットとなるプレイヤーが決定されており、そのプレイヤーに対して攻撃する。エンダードラゴンは、エンダードラゴンに最も近い位置のパスノードとプレイヤーに最も近い位置のパスノードを選択しながら、プレイヤーに近づくように移動する。また、フェーズが移行する前にこのパス移動が完了した場合は HoldingPattern と同じロジックでパスノードとターゲットの選択が繰り返される。

ターゲットとの距離が64ブロックより近く、5tick以上ターゲットを視認できる場合にドラゴンの火の玉エンティティを作成し、攻撃を行う。その後、その時行っていたパスの移動を完了させると HoldingPattern に移行する。

プレイヤーのエンティティが存在しなくなった場合はすぐに HoldingPattern に移行する。

着陸

Perchに着陸するまでの状態。

LandingApproach

Perchに着陸する前のフェーズ。まず、エンダードラゴンと最も近いプレイヤーを選出する。その後、Perchを原点としてプレイヤーの反対側にあるパスノードの直上に移動する(これはおおよそ黒曜石の柱の上部や上空を指すことになる)。

移動が完了したタイミングでそのまま Landing に移行する。

Landing

Perchに着陸するフェーズ。(よほど変な地形や状況で無い限りは)高度を保ちながら (X: 0, Z: 0) に向かって進み、その後はPerchの先端を目指して周期的に回転しながら降下してくる。

Perchの先端からの距離が1ブロック未満になったタイミングで SittingScanning に移行する。また、このタイミングで Sitting 系のフェーズによる連続攻撃回数の記録はリセットされる(詳しくは後述)。

着陸後

ブレス攻撃をするための状態。ブレス攻撃は連続で4回までしか行わない。

この状態にいる時に50ダメージ以上与えると、即座に Takeoff に移行する。

SittingScanning

Perchの先端に留まり、特に大きくは動かないフェーズ。20ブロック以内にプレイヤーがいる状態では、そのプレイヤーの方向を向く。

20ブロック以内にプレイヤーがいる状態が25tickを超えた場合、 SittingAttacking に移行する。

もし20ブロック以内にプレイヤーいない状態で100tickが経過した場合、150ブロック以内のプレイヤーを捜索し、発見できれば ChargingPlayer に移行する。発見できない場合は Takeoff に移行する。

SittingAttacking

ブレス攻撃前のフェーズ。特に何もしない。クライアント側ではエンダードラゴンの咆哮が再生される。

40tick経過後に SittingFlaming に移行する。

SittingFlaming

ブレス攻撃を行うフェーズ。10tick経過時、ブレスを展開する。

200tick経過後、連続攻撃回数が4回未満の場合は再度 SttingScanning に移行する。4回以上の場合は Takeoff に移行する。

離陸

Perchから離れ、HoldingPattern に戻るまでの状態。

ChargingPlayer

突進攻撃をしてくるフェーズ。SittingScanning から移行してくる時に事前にターゲットとなる経路が決定されており、目的の座標に対して攻撃する。

フェーズ中、ターゲットがリセットされるなどすると、すぐに HoldingPattern に移行する(おそらくこのフェーズ中にワールドに入りなおすなどすると発生する?)。

ある程度の時間その方向に向かったら HoldingPattern に移行する。ある程度の時間はデフォルトでは10tickだが、エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い 状態になっているtickがある場合は 1tick ずつ時間が減っていく。

Takeoff

突撃をせず、そのまま HoldingPattern に戻るためのフェーズ。現在地から最も近いパスノード → おおよそ反対側の上空という経路で地上から離れる。

Perchから10ブロック以上離れたタイミングで HoldingPattern に移行する。

RTA向けのTips

自分のRTAの手腕はまだまだなので見当違いなことを言っている可能性はあるが、あくまで今回の調査から得られたTipsということで、いくつか紹介する。

StrafePlayer に移行しづらくする

RTAの最中、エンダードラゴンがすばやくPerchに着陸してもらうことが求められる。このためには、まず LandingApproach フェーズに移行してもらう必要がある。

HoldingPattern で説明したように、基本的にPerchから 150ブロック以上離れている10ブロックより近くにいる 場合に次のフェーズに移行するチャンスを得る。ただ、その際は LandingApproach の抽選と同時に StrafePlayer の抽選もされてしまう。StrafePlayer に移行すると、エンダードラゴンとプレイヤーの距離が64ブロック未満になるまで火の玉を発射せず、HoldingPattern に戻ることもない。そのため、できるだけ StrafePlayer への抽選を避けることが無難な行動だと言える。

この際、StrafePlayer の抽選にはプレイヤーの距離も抽選に関わる。

 \frac{1}{\left\lfloor \frac{(\text{Distance between Player and Perch})^2}{512} \right\rfloor + 2.0} \quad

遠ければ遠いほど確率は下がるので、StrafePlayer の抽選をできるだけ外しやすくするためには エンダードラゴンとの距離を遠くする ことが重要となる。

また、StrafePlayer が選択されてしまった場合のことも考える。エンダードラゴンは垂直方向へのスピードより、水平方向へのスピードの方が早いため、プレイヤーがPerchとの距離を稼ぐ場合は縦方向に遠ざかることでより早く火の玉を発射してもらうことに期待できる。この場合、黒曜石の柱に登り、そのまま上方向にブロックを積むことで、火の玉を避ける行動も兼ねて距離を稼ぐことができる。

また、エンドクリスタルを破壊すると、LandingApproach が当選する確率と StrafePlayer が当選する確率の両方が上がる。これも上記アプローチによってうまく回避できると言える。

 \quad \frac{1}{\text{Count of End Crystals} + 2}

Landing を素早く終わらせる

Landing の際に向かう目標とされるブロックは Landing に移行する際に決定される。そのため、移行前にPerch上にどんどんブロックを置いていき、柱を延長するような形にすることでそのブロック上に着地するようになる。これにより、通常よりもエンダードラゴンが着地するまでの時間が短縮される。

ただ、これによって短縮される時間というのはそこまで有意なものとは思えず、これをやるのであれば、もうちょっと別の技(例えば: Zero Cycle など)を練習してもらったほうが良いように思う。

おわりに

今回はエンダードラゴンの挙動を実装した各フェーズのクラスを中心にコードリーディングすることで自分が知りたい範囲で詳細を確認することができた。これにより、ある程度は自信を持って行動できるようになると思う。

やはりMinecraftのゲーム内のロジックを読み解くのはかなり楽しい。これは、Minecraft自体がとても機械的で、かつそのロジックが充分に機能していることが大きく、その裏側を見ることで自分のMinecraftに対する知識の確かな裏付けとなる感覚があるからだと思っている。表面から見て感情がなく、とても律儀に動くゲームの裏側は勿論コードによって支えられており、その実装がとてもしっかりしているからこそ、ここまで多くの人がこの箱庭に魅了されたのだ!と胸を張って言えるような気がする。

また、そのプレイ人口の数もあってエコシステムがかなりしっかりしているため、何かをいじったりするための用意が周到なのも特徴と言える。おおよそなんでもコードレベルで知ることができるような気持ちになる。

今後もなんらかMinecraftにまつわる不思議なことが現れるたびにコードを見に行って、感心したり納得したりしながら付き合っていくのだろう。

*1:この用語自体はRTAでも使われるものだが、通常のRTAでは岩盤の柱の上にブロックを積むようなことはしないため、コミュニティ内で高い位置に置かれたブロックのことまで含めてPerchと呼ぶのかは不明だが、今回は便宜上Perchと呼ぶ。

誕生日を迎えた 2023/11/21 と 2023/11/22 が楽しみすぎる

25歳の誕生日を無事に迎えることができた。このあいだおじいちゃんが死んでからずっと1年生きることが素晴らしいことに思えてどうしようもなく、実際にちゃんとこの日は嬉しかった。昔だったら 20,november をやりにいって喜びを消費していたのだと思うけど、もう弐寺はやってないのでゲーセンには行かなかった。

ただ、だからといって具体的になにか特別なイベントが発生するわけでもなく*1、寝る前にマリオワンダーの続きでもやろうかな~とか思ってたんですが……↓↓↓


思い出から始めます

当時*2、ヘボコンというものが開催されていて、その紹介動画に使われていた曲の正体がどうしても知りたくて探し当てたのがアルバム Hercelot - Wakeup Fakepop で、そのアルバムの他の曲も含めて当時聞いていた音楽の形態と全く違う、本当に凄いものが並んでいたのでめちゃくちゃ驚いていた覚えがある。

maltinerecords.cs8.biz

そもそも無料で音楽のバイナリを手に入れることについて、にこさうんどかyoutubemp3あたりか、もっと英語なサイトで勝手に落とすくらいしか体験がなくて、「こんなにすごいものを正式に無料で手に入れられるのか」という驚きもあってとても強く記憶に残っているアルバムだった。また、そこから周辺情報を調べたタイミングが、自分がはじめて Maltine Records を認識したタイミングだったりした。で、当然新しいアルバムを聞き、すごいな~とか思ったり、周辺にあるリリースを探したりするんだけど、ちょうど Wakeup Fakepop の真下にあったものが bo en - pale machine だった。

maltinerecords.cs8.biz

さっきのアルバムの真下にあるから同じ方向性を期待したら全然そうではなく、ただとても意外性があり、あまりにもミュージカルな感じの展開と展開からは全然予想してなかった激しい音にとても大きく心を動かされた記憶がある。というか、この Maltine Records ってマジでなんなんだ、みたいな気持ちになっていたと思う。どういう振れ幅でこれらの音楽が集まってきたんだ、みたいな。


少し話を飛ばす。そのちょっと後くらいの頃、別件で Maroon 5 がカバーした Pure Imagination について色々探していたタイミングで、以下のような動画をみつけた。

www.youtube.com

これで OMORI を知ることになった。すごく面白そうなテーマのゲームが出るのだ、と知った。ただ、このゲームはかなり難の産で開発続報がかなり遅れてしまっており、しかもkickstarter発のプロジェクトとしてしまったので、もしかすると金を払ったのに何も貰えないかもしれないbackerたちは当然のように怒り始め、コメント欄はとんでもない激詰めを見るための場所みたいな感じになっていたり。まあそれも含めて、背景を知れば知るほど OMORI って Pure Imagination なのかもな、と思っていたり。

で、OMORI についてのことを探していたタイミングで bo en - pale machine に収録されていた my time (を、ちょっとピッチアップしたもの) が OMORI の PV として使われていることを知った。

www.youtube.com

偶然にも、自分の心を打った曲が興味を持っているゲームと紐づいたのでめちゃくちゃ面白くて、一人で pale machine を何度も聞いたり、とても面白かった覚えがある。


それからしばらく経って、自分が東京に出て、音楽イベントに入り浸るようになった時、当時開催されていたMALTINE SEED STAGEというイベントシリーズの番外編としてMALTINE SEED FLOORが開催されることになる。

bo enが緊急来日 “MALTINE SEED”番外編でDE DE MOUSE、Avec Avec、長谷川白紙らと共演 - Spincoaster (スピンコースター)

長谷川白紙*3はこれの少し前のイベント出演時に「しばらくイベントには出ない」ような旨と言っていたから何故か参加しているし、Hercelot、bo enの両名については前述したように自分のなかで勝手に文脈を持っていたのでかなりびっくりした覚えがある。

ここで生演奏の miss you と my time を聞くことになって大きく感情を動かされ、このタイミングでアルバム内でこの2曲が自分の中でとても大きな印象を持つ曲になり、同時に pale machine 自体がさらに輝くアルバムとなってしまった。

それからまたしばらく経って、超唐突にOMORIのリリース告知を聞くことになる。

かなりびっくりした。あれだけ叩かれていたし、そのままフェードアウトするんだろうな~と思っていた OMORI がちゃんとリリースされるらしいこと*4もそうだし、bo en が制作として携わることも大きく印象に残った。自分の中で OMORI to bo en の紐づけがとても強固になったタイミングだった思う。


それで、しばらく経ってちゃんとリリースされ、bo en のゲームプレイを見てから OMORI をプレイし*5、かなりしっかりやられたりして、またしばらく経って OMORI のコンサートが開催されることになったが参加できなかったり……(OMORIについての話はここじゃないところで書くべきなので、別で書きます)

で、OMORI 3周年記念コンサートが開催されることになる。これはちゃんとチケットを入手できて、SS席を得ることができた。開催は明日 2023/11/22 の昼。思い入れのあるゲームのコンサートってどんな感じなんだろう、超楽しみ。

musicengine-info.net

それで、bo en - pale machine の10周年記念リマスターアルバムの発売とイベント開催の告知が流れる。

boen.bandcamp.com

イベント開催は 2023/11/21 の夜。「え、ここ連日にするんですか?」と何かを感じつつ…… 思い出のアルバムを記念するイベントなんて、そりゃ超楽しみ。

んで、急にこのイベントが pale machine 2 のリリースイベントであると明かされる。マジかよ、僕の大好きなアルバムが2になるんですか?ちょっと信じられない。

これ自体のリリースは 2023/11/21 の 0:00 で、つまり今日(2023/11/20)ちょっと夜起きてれば聞けるじゃん!!!!!となり、実際に pale machine 2 はリリースされ、0:35頃にすべて聞き終えた。

open.spotify.com

思い入れというものはとても恐ろしいもので、人間をニュートラルからズラしてしまう力があると思ってるんだけど、ここでその力が発揮されて、Prelude の最後に I Still Miss You (miss you の Tomggg リミックス) の頭出しが流れたタイミングから、最後までずっと涙を流し続けてしまった。

一つ前の曲のラストで頭出しが流れる演出は pale machine からのものだが、同じように踏襲していることと、本当に miss you のリミックスが聞けるんだ、という期待から気持ちが暴発してくれた上に、最後まで通して実際に自分の思い出を大きく揺さぶってくるような素晴らしいリミックスばかりで、ずっと泣いていた。特に Prelude、I Still Miss You、Be Okay' 23、Our Time (my time の PAS TASTA リミックス)、は特に凄かったが、それ以前に最初から最後までずっと凄かった。曲単位で感情を大きく動かされて泣くことはよくあるんだけど、アルバムを通してここまで泣き続けることってそんなになくて、体験としてものすごかった。


今日は誕生日だったから、ほんとはケーキとかを買おうとしてたんだけど、最近の睡眠の傾向として一日中起きていることが困難なために夕方ちかくに寝始めてしまい、結果的にはこれが原因でケーキを買うことができなかった。ちょっとだけ悲しかったけど、pale machine 2 が完全に誕生日プレゼントとして機能していて、今日一日のすべてが素晴らしいと言い切れるくらいには元気になってしまった。

しかも16時間後に開催されるイベントでは、大きな音でこれを聞くことができる。その翌日は OMORI のコンサート。自分の誕生日がある週としてここまで素晴らしいことってあまりないと思う。本当に楽しみな気持ちに包まれながら、これをどうにか沈めて頑張って寝る。みんなおやすみ。

*1:あっ、みなさんからたくさんのリプは頂いたな!!!!めちゃくちゃ嬉しかった!!!!!!!!

*2:たぶん2014年の最初?

*3:僕が人生で初めて参加した音楽イベントであるMALTINE SEED STAGEのvol.1に長谷川白紙は初ライブとして参加していた上に、同い年であるということを知って大いにびっくりした覚えがある。自分と同い年の人間がステージに立ち、しかもこのレベルの演奏を披露される、みたいな「明らかに自分より凄い同い年」という体験を初めてしたから。それ以後、ずっと長谷川白紙のことが好きで追いかけ続けていた。

*4:制作者であるOMOCATがOMORIを完成させたことは、何かの創作をやったことある自分としては本当に強く称賛したい行為だと思っている。

*5:普通に初見でやればよかったのに……

Twitterというサービスは難しいだろう、という自分の考えを一旦ここにまとめてみる。身内向け記事で、この記事内では何も解決していない。


まず、技術要件的にTwitterというサービスはとても複雑で、そこらの企業がサッと真似できるようなサービスではない。小さい範囲で見ればなにかのフレームワークのチュートリアルでやるような話にも見えるが、これを莫大な数字とともに動作を担保していく、というのはとても大変な話だろう。

以下に例を挙げてみる。

blog.twitter.com のInfrastructureに存在する記事を見ていると、基本的にはスケーラビリティを確保し、コスト面やメンテナビリティにも気を遣いながら、技術的に正しくアプローチしようといった基本的な姿勢が伺える。また、既存の環境でさし当たった問題に対して真っ当かつ専門性も高めに新しい構造を設計している。少なくとも「この規模の数のツイートを処理しなければならない」という課題に直面している企業は世界中を探しても有数で、その中でもTwitter社は(技術的には)かなりうまくやっているように思う。


ただ、運用面に関して言えば、ここ数年(もしかしたら十数年)のTwitterはうまくいっていない、と評価される。例えばタイムライン上の広告出現、User Streams APIの廃止、おすすめタブ、スパム発生の常態化、一部APIの有料化、イーロンマスク、API全体の有料化、サードパーティ製クライアントの追い出し、表示系APIの激しいレートリミットなど。これは今サッと思い出せたものでしかなくて、もっとたくさんある。

Twitter社は我々に嫌がらせをするためだけにこれらの変更を加えているのだろうか?僕はTwitter社に関わりのある人間ではないが、それでも断じて「違う」と言えるだろう。では、何故このような変更を加えてしまうのだろうか?中には(自分の主観でしかないが)おおよそのユーザーの理念に反する大きな変更も存在したが、それを行う理由があると思う。

端的に、Twitterというサービスを運用するにはあまりにも大きなコストが掛かるのだと思う。

考えてみると上記の変更は全てお金に関わるもののように思う(思い込みかもしれないが)。ユーザーストリームなんて、世界中の人間がセッションを貼り続ければ重くなって全部切りたくなるに決まっているし、おすすめタブで炎上ツイートを表示して我々を釘付けにし、その間に挟まる広告の回転率も高めたくなるのだろう。

最近では、イーロン・マスクがTwitter社を購入したと思ったら、直後に大規模なレイオフを行った。このタイミングでSREの面々や、自分が尊敬していたエンジニアもその対象になっており、個人的には現在でもかなりの疑問を呈したい結果となっているが、一方ではそれくらいのことをしなければならないほどの窮地に陥っていたのではないかと思う。なんだったら今頃本当にTwitterが消えてしまった!という世界線も考えられたのではないか。

つまり、最初から大量だったツイート/ユーザーが、人気に応じてさらに増えてしまった結果、技術面ではどうしようもなくなってしまい、運用的な面での自転車操業を余儀なくされているのではないか、と考える。顧客のスケールに現在の世界の技術力が追いついていないのではないか。

この傾向はイーロン・マスク以後は尚更顕著である。例えば、先日はスパムに対する一時対応として「ログインしていないと個別のツイートが見れない状態にした」と明かしている。おそらくは有料APIを使わないスパム企業に対するスクレイピング対策だろうと思うが、このような対応は得てしていたちごっこを繰り返すだけで、根本解決には至らないだろう。

実際にこれは尾を引いており、昨日くらいから現在にかけてはかなり激しいレートリミットを仕掛けられる結果となっている*1。これに対して周りのTwitterユーザーはまたしても「誰かが立てている非中央集権的サービスへの移行騒ぎ」を起こしているが、これは結局現在のTwitterの互換にはならないし*2、Twitterほどのスケーラビリティを確保できていないので人が集まればすぐにパンクする。

先程書いた通り、イーロン・マスク以後にこのような傾向が現れるようになったわけだが、イーロン・マスクが全ての元凶であるとは思えず、むしろ潜在的に苦しい状況だったところを打破しようとしているのではないかとまで思う。今朝のイーロン・マスクのSEXツイートも、彼のキャラ性質を活かしたヘイト管理に過ぎないのではなかろうか。

以下の記事の、以下の部分がとても虚しい。

The Infrastructure Behind Twitter: Scale

There is no such a thing as a temporary change or workaround: In most cases, workarounds are tech debt.

(意訳) 一時的な変更や対処法なんて存在しない:ほとんどの場合、そのような対処法は技術的な負債となる。

(基本的な収入源としての広告も一向に良くならないのは何故なのだろう、個人的にはInstagramのような、もう少しまともな広告であってほしいと思うが、Twitterではそれは難しいのだろうか。)


ただ、そんな状況下であったとしても、僕は知り合いが好き勝手にツイートできるような環境を用意したTwitterが好きで、これは技術的/運営的な側面から自分には用意することができない環境でもある。これに対しての感謝や尊敬の気持ちとして常々Twitter社に課金したい気持ちが強く、なのでTwitter Blueに加入している。Blue自体の機能を受け取りたいためにBlueに加入しているわけではないためとても不健全に思えるが、もはや自分にはこのくらいしかすることがない。TwitterにTwitterとして居て欲しいだけなのに。とっても非力で、本当に悲しい。


過去に何度か、Sample Stream APIを利用して世界に流れるツイートのうちランダムな1%を見続けていたことがあるが、その時の思い出としてはスパム、チンコ、おっぱい、マンコといった感じで、ほとんど意味を成していなかったように思う。その時ですらそんな有様だったので、現在流れているツイートやユーザーのうち、無視できない割合がスパムによる負荷なのではないかと思う[要出典]。これらを積極的に削除するような運用面でのリソースはもはやTwitterには残っていないだろう。

Twitterはその手軽さからみんなに愛されるようになり、長年にかけて空気感を構築してきたのだと思うが、そんなTwitterの善意に肖って意味のない金稼ぎをし続ける企業や個人がいて、もしもそいつらがこの場を崩そうとしている[要出典]のであれば、それは本当に悲しい。ほとんどが善意ではなく、悪意に対してリソースを割くことになっていたのなら、そいつらがTwitterを壊している、ということにもつながるだろう。だったら本当に許せないね〜、悲しい。

*1:個人的な見立てでは、これは来週には忘れ去られていて、普通に動いているだろうと思う

*2:Twitterと並行して存在しうるものだと思っている

TypeScript 5.1のJSX.ElementTypeと各種ライブラリでの対応 @types/reactとか@emotion/reactとか

こんな感じのエラーを "@types/react": "^17.0" を指定せずに倒せるようになった!!!

Type error: 'SomeComponent' cannot be used as a JSX component.
  Its return type 'ReactNode' is not a valid JSX element.
    Type 'undefined' is not assignable to type 'Element | null'.

まとめ

一番最初に対応方法のまとめを書いておく

  • jsxImportSource を弄っていない場合
    • typesciprt を 5.1、@types/react を 18.2.8 にアップデートする
  • jsxImportSource を弄っている場合
    • typesciprt を 5.1、@types/react を 18.2.8 にアップデートする
    • JSX.ElementType を宣言する

例えば2023年6月4日時点で @emotion/react を使っている場合、アップデートを行った後に以下のように宣言する。

// src/types/emotion-react-jsx-runtime.d.ts
import { JSX } from '@emotion/react/jsx-runtime';

declare module '@emotion/react/jsx-runtime' {
  namespace JSX {
    type ElementType = React.JSX.ElementType;
  }
}

TypeScript 5.1 JSX.ElementType

TypeScript 5.1がリリースされた。この新機能の一つとして、JSXのElementとして妥当かどうか確かめるための新しい型 JSX.ElementType の存在が導入された。

これまで、関数コンポーネントは null | JSX.Element を返すことが強制されていた。例えば、const Component = () => 42; <Component /> という例での Component は42を返し、これは null | JSX.Element には該当しないため、型チェックの段階で弾かれる。

ただ、Reactで作成する関数コンポーネントは ReactNode を返す。この型はおおよそ Element | number | string | Iterable<ReactNode> | undefined (将来的には Promise<ReactNode>) のようになっており、上記で記した型と一致しない。ここで、ReactNode も問題なくコンポーネントとして利用できるようにしたいモチベーションが発生する。

この解決策として、TypeScript 5.1以降より、TypeScriptのjsx周辺で型 JSX.ElementType を使うようになった。各種 jsx-runtime を提供するライブラリは JSX.ElementType を埋めるように宣言をする必要がある。JSX.ElementType を使ってコンポーネントを検査した結果、パスしたのであれば、そのコンポーネントはそのランタイムで使用可能だということが分かる、という流れ。例えばReactでこれを用意するなら以下のような感じになる。

// inlined `React.JSXElementConstructor`
type ReactJSXElementConstructor<Props> =
  | ((props: Props) => React.ReactNode)
  | (new (props: Props) => React.Component<Props, any>);

declare global {
  namespace JSX {
    type ElementType = string | ReactJSXElementConstructor<any>;
  }
}

stringや、ReactNode を返す関数コンポーネントや、クラスコンポーネントに対応している型を用意し、JSX.ElementType として宣言する。これで、ReactのJSXランタイムは、Reactで作成するような関数コンポーネントをそのまま扱えるようになった。

github.com

@types/react 18.2.8 JSX.ElementType

実際に、@types/react 18.2.8 のリリースでは、コンポーネントの返り値として ReactNode を直接利用できるような ElementType を宣言するようになった。抜粋するとこんなかんじ。

type JSXElementConstructor<P> =
  | ((props: P, deprecatedLegacyContext?: any) => ReactNode)
  | (new (props: P) => Component<any, any>);

namespace JSX {
  type ElementType = string | React.JSXElementConstructor<any>;
}

index.d.ts

github.com

怒涛のCloses

Emotionで JSX.ElementType に対応してみる

追記: 2023年6月7日に下部のセクションで出したPRがマージされたので、@emotion/react がv11.11.1より大きければ既に対応されているはず!!

これで万事解決かと思い、自分のNext.jsのプロジェクト内の typescript5.1.3 に、@types/react18.2.8 にアップグレードしたが、変わらず同じ問題が発生した。端的に言うと、Emotionが関連した問題だった。

今回のプロジェクトではEmotionを使用している。Emotionは tsconfig.jsonjsxImportSource に対して @emotion/react などと指定している。

@emotion/react では、コンポーネントのpropとして新規に css が指定できるようになっている。この解決のため、任意の方法でjsxのランタイムを差し替え、新規に css propを指定できるように変更が入っている。差し替え方法の一例としては上記のように、jsxImportSource@emotion/react を指定する。これによってjsxのランタイムとして @emotion/react/jsx-runtime が利用されるようになる。

ランタイム内では、基本的にはReactのランタイムのJSXをそのまま利用しているが、IntrinsicElements に対しては追加で手を入れており、これによって css prop の利用を実現している。

// 事前に `type ReactJSXElement = JSX.Element` みたいにReactが宣言した型を `ReactXXXX` の型に再代入している

// ↓後に JSX という名前でexportされる
export namespace EmotionJSX {
  interface Element extends ReactJSXElement {}
  interface ElementClass extends ReactJSXElementClass {}
  interface ElementAttributesProperty
    extends ReactJSXElementAttributesProperty {}
  interface ElementChildrenAttribute extends ReactJSXElementChildrenAttribute {}

  type LibraryManagedAttributes<C, P> = WithConditionalCSSProp<P> &
    ReactJSXLibraryManagedAttributes<C, P>

  interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {}
  interface IntrinsicClassAttributes<T>
    extends ReactJSXIntrinsicClassAttributes<T> {}

  // ここで `css` prop を使えるようにしている!
  type IntrinsicElements = {
    [K in keyof ReactJSXIntrinsicElements]: ReactJSXIntrinsicElements[K] & {
      css?: Interpolation<Theme>
    }
  }
}

このあたり

ここで問題なのは、@emotion/react/jsx-runtime が提供するJSXには、2023年6月4日時点では ElementType が宣言されていないということだった。ただ、先程提示した通り、基本的にはReactが宣言した型をそのまま使いまわしているだけなので、追加で ElementType を宣言してやれば良い。もちろん、事前に typescript を 5.1 に、 @types/react を 18.2.8 にアップデートしておく必要がある。

// Emotionが定義したJSXをそのまま持ってくる
import { JSX } from '@emotion/react/jsx-runtime';

declare module '@emotion/react/jsx-runtime' {
  namespace JSX {
    // 追加で `ElementType` を宣言する
    type ElementType = React.JSX.ElementType;
  }
}

Emotion側に対応してもらいたい

追記: マージされた!!!!!!v11.11.1からは素の状態で対応されているはず。

せっかくならEmotion側で対応してほしい。

2023年6月4日時点の @emotion/react からの @types/react への依存状況はオプショナルなものになっており、しかもバージョンの指定も存在しない。なので、18.2.8以上に依存することがないように ElementType をライブラリ内で宣言するような形で対応するPRを出した。

github.com

正直、JS関連のライブラリに対する文化などを知らないので、これが歓迎されるものなのかはわからないが、マージされればいいな~くらいの気持ち。マージされたら最初に書いた対応も必要なくなりそう。