この記事はニンテンドーDS向けエミュレータの一つである「DeSmuME」(以降Desmume)にLuaを導入し、解析やTAS制作を手助ける方法を知る記事です。
注意
この記事ではエミュレータの導入については取り扱いません。導入については前回の記事を参照ください。
→https://qiita.com/gestoC_moti/items/ac1863f25a225066a043
Desmumeの機能のmemory search
やView memory
で目的メモリ番地を特定し、memory watch
でメモリ値を監視することができる。しかしこれはメモリ値を監視するだけで、四則演算等の処理をかけることはできず解析で困ることが多々ある。
そこでDesmumeの機能としてLuaスクリプト
を使用することができるのでそれを活用して解析を手助ける。
Luaスクリプトとはなんぞや
Luaスクリプト
(これ以降Lua
)は1993年1に開発されたスクリプト言語で現在も開発が進んでいる。動的型付け言語であるものの実行は早く、C言語をホストプログラムとして組込むことを想定した言語であるためC言語系と相性が良い2(他言語でも組み込み可)
現行バージョンは5.4.x
になるが、今回用いるdesmumeは5.1
に対応しているので、5.2
以降の機能は使えない。
例:bit32
ライブラリは使用できない。代わりにbit
ライブラリを使用する必要がる。
リファレンスマニュアル(英語)は以下の通りである。
https://www.lua.org/manual/5.1/
日本語で読みたい場合は有志が翻訳したものがたくさんあったはずだが、軒並みSSLエラーや404notfoundが返ってくるためWebArchiveなどで探す必要がある。(現代の機械翻訳の精度はまあまあ高いのでそれで読んでしまうのは多いにありではある。)
Luaスクリプト 環境構築
Lua
をDesmumeで動かそうとLuaスクリプトを読み込ませても実は動かない。最初の.zip
にはLuaのDLLファイルが含まれていないためだ。そこでDLLの入手から行う。
(あなたがもしlua51-64bit.dll
を持っているのであれば手順1は飛ばしてください。動画編集ソフトであるAviutl
を導入しているのであればAviutlのプラグインで入手している可能性があります。3)
導入したDesmumeが32bit版のものであれば、32bit版のlua51.dllを入手してください。
手順1(安全性が高い[少し面倒])
公式サイトからダウンロードする方法です。過去のバージョンになるので少し面倒です。 ダウンロード後はほとんどのファイルが不必要なものです。
Luaの公式サイトである以下のページからLuaBinaries 5.1.5 - Release 1
のlua5_1_5_Win64_dll8_lib.zip
をクリックする。
https://luabinaries.sourceforge.net/download.html
その後SOURCEFORGEに飛ぶので5.1.5
→Windows Libraries
→Dynamic
でディレクトリ移動をし、lua-5.1.5_Win64_dll17_lib.zip
をダウンロードする。
解凍後DLL以外は削除してかまいません。
手順1(安全性が低い[とても簡単])
必要なファイルだけをダウンロードするだけでとても簡単ですが、そのサイトを信用するかは自己責任です。
好きな検索エンジンでlua51 dll 64bit
と検索をしてlua51-64bit.dll
などのバージョンが5.1
で64bit版
を入手してください。dllをダウンロードするためそのサイトを信用するかは自己責任です。
手順2
手順1にて入手した64bit版lua51.dllをlua51.dll
にリネームを行い、Desmumeがあるディレクトリにファイルを置く。
これで環境構築は行えました。まずはテストプログラムを動かして導入できているかを確認します。
メモ帳を開き以下のコードを書きます。メモ帳以外のコードエディタを使用している場合は言語をLuaに設定しコードを書き始めてください。
筆者はメモ帳でプログラミングする人間なのでその前提で記事を書きます。特に理由がなければvscodeあたりでも使ってください。
vscodeブラウザ版→https://vscode.dev/
local function Fn()
gui.text(0,10,string.format("Hello World!"))
end
gui.register(Fn)
標準入出力を使わない上に関数を使用しているなど突っ込みたいことが山ほどあるでしょうが、理由はあとで説明します。
名前を付けて保存からtest.lua
を保存します。Luaスクリプトの拡張子は.lua
です。保存先はDesmumeを置いている同ディレクトリにluaフォルダを作成しその中に保存するのがオススメです。
どこに保存しても構いませんが、パスに2byte文字を含めてはなりません。
Desmumeを起動し、Tools
->Lua Scripting
->New Lua Script Window ...
を選択し以下のウィンドウを表示させる。
Browse...
から先ほど保存させたtest.lua
を選択する。
ウィンドウのOutput Consoleにscript returned but is still running registered functions
が出力されるか、下記の図のように下画面上部にHello World!が出力されていることを確認してください。
この時点でlua51.dllに関するエラー文がOS側から出力された場合は導入手順1から見直してください。
Luaを使ってメモリ値を表示させるプログラムを書く前に必要な知識
ifやwhileなどの基本的な文の書き方は先に述べたリファレンスマニュアルを参照してください。ここではDesmumeで表示させる場合の基礎知識を述べます。
文字列主力先
Desmumeでluaを使う際に指定できる文字列出力先は3つで、
stdout
output console
guiAPI
の3つです。
stdout
はLuaScriptウィンドウのstdoutチェックボックスから有効化するとそちらに出力するようになります。Windowsのコンソールに出力されます。コンソールウィンドウはTools -> Console -> Enableから表示できます。エミュレーターのあらゆる出力がされるのでメモリ値管理などには向いていません。
output console
はLuaScriptウィンドウに出力されます。エラー文を実装した場合などに出力をする場合もありますが、リアルタイムでのメモリ値表示には向いていません。
guiAPI
はDesmumeで用意されたAPIで先ほどのtest.lua
のように画面に重ねて表示ができるものになります。メモリ値の表示には一般的にはこのAPIを利用することが多いでしょう。
関数のループ
メインの関数はループさせることが望ましいです。ループをさせないとどのようになるか、以下のコードを実行して確認しましょう。
gui.text(0,10,string.format("Hello World!"))
実行をすると約1秒ほど表示された後に文字が消えます。表示させる続ける場合には実行をループさせる必要があります。主に以下のループ方式があります。
-- ここに宣言
while true do
-- ここにループさせたい素敵なコードを書く
emu.frameadvance()
end
--ここに宣言
function fn()
--ここにループさせたい素晴らしいコード書く
end
gui.register(fn)
ループさせたい場所にgui.text(0,10,string.format("Hello World!"))
を追加して実行してみると、どちらもHello World!
が表示され続ける。最初の形式が一般的ですが、Desmumeの場合他のエミュレーターと異なる方法で動作しているので、2番目の方法でループさせる必要があります。4
ループの際に呼び出すレジスタ関数
Desmumeに限らず、luaがサポートされているどの関数を呼び出すかを把握している必要があります。
emu.frameadvance()
while true do --- endの間に入れて動作させます。厳密にはレジスタ関数ではなくフレームを進めるAPIを呼び出しているだけです。この関数を使うメリットは確実に全てのフレームで更新されることです。
ただしDesmumeでは決して使ってはなりません。フレームカウンターは増えていきますが、描画更新されないため全く役に立たない関数です。またこれを入れたluaを動作させながらのTAS制作はdesyncを誘発する可能性があるので決して使用してはなりません。
-- ここに宣言
while true do
-- ここにループさせたい素敵なコードを書く
emu.frameadvance()
end
gui.register(function)
この関数はエミュレータが描画を更新するたびに呼び出されます。全てのエミュレータで全フレーム毎に呼び出される保証はありません。一時停止などを行わない場合は重大な情報を見逃したり、メモリ値をトリガーに作動するコードを書いた場合に上手く認識できない場合があります。本来はフレーム毎に呼び出される関数ですが、Desmumeの場合は常に呼び出されるため非常に便利な関数であり、Desmumeの場合はこれを使うべきです。
--ここに宣言
function fn()
--ここにループさせたい素晴らしいコードを書く
end
gui.register(fn)
emu.registerbefore(function)
この関数はエミュレータが描画をする直前に呼び出される関数です。一時停止をすると呼び出されず文字列出力しないため、Desmumeの場合はbot制作に向いている関数ですが、メモリ表示には向いていません。
--ここに宣言
local function Fn()
--ここにbotのための良いコードを書く
end
emu.registerbefore(Fn)
emu.registerafter(function)
この関数はエミュレータが描画をした直後に呼び出される関数です。emu.registerbefore()
と同じく一時停止をすると呼び出されないため使い方に注意です。
--ここに宣言
local function Fn()
--ここにbotのためのすごいコードを書く
end
emu.registerafter(Fn)
メモリ値の表示
メモリ値の表示にはmemory search
で予め探しておいたメモリ番地が必要です。
流れとしてはmemory search
でメモリ番地の特定→memory
ライブラリで読み込み→gui.text
で表示が流れです。前回の記事ではNewスーパーマリオブラザーズDSでのX座標とY座標のメモリ番地を特定しているのでこれを画面に表示させるサンプルコードは以下の通りです。
memory searchとは何ぞやとなった場合は前回の記事後半部を参照ください。
→https://qiita.com/gestoC_moti/items/ac1863f25a225066a043
function fn()
local X,Y
X = memory.readdword(0x0208AD20)
Y = memory.readdword(0x0208AD24)
gui.text(0,10,"X: "..X)
gui.text(0,20,"Y: "..Y)
end
gui.register(fn)
表示されている値は何も変換をしていないので大きな数字が表示されていますが、これが基本形です。
memoryライブラリ
memoryライブラリには31個の関数が登録されていますが、ここでは読み込みでよく使うものを説明します。5
memory.readbyte()
:引数に指定したメモリ番地の1byteのメモリ値を戻り値として返します。
memory.readword()
:引数に指定したメモリ番地の2byte(half word)のメモリ値を戻り値として返します。
memory.readdword()
:引数に指定したメモリ番地の4byte(word)のメモリ値を戻り値として返します。
guiライブラリ
guiライブラリは15個の関数が登録されていますが、ここでもよく使われるものを説明します。6
gui.register()
:引数に指定した関数を画面描画毎に呼び出しその結果が戻り値して返ってきます。ループして使用(上の文を参照)
gui.text(int X,int Y, string msg[, type color1, type color2])
:テキストを描画する関数です。引数の'int X'には表示させたいX座標,int Y
には表示させたいY座標を指定します。color1
color2
はオプションで省略可能。指定時は0xRRGGBBAA
の16進数で指定するか、blue
などの名前で指定します。 color1
は主な色で省略した場合は白が、color2
は輪郭の色で省略した場合は黒が指定されます。
gui.pixel(int x, int y, type color)
:1pixelを描画する関数です。
gui.line(int x1, int y1, int x2, int y2, type color)
:X1Y1座標からX2Y2座標まで1pixelの線を描画する関数です。
gui.box(int x1, int y1, int x2, int y2, type FillColor, type BorderColor)
:X1Y1座標からX2Y2座標まで塗りつぶされたボックスを表示します。FillColor
に塗りつぶす色を指定し、BorderColor
に枠線を指定します。BorderColor
は省略可能で省略した場合、FillColor
が枠線に使用され、FillColor
の半透明色で塗りつぶされます。
int X
などは具体的な数値も指定できますが、関数なのでローカル変数やグローバル変数も指定できます。
string msg
はstring.formatを使用して指定ができるため変数の表示や書式指定が可能です。
サンプルコードはtest.lua
(最初にHello World!を表示させたコード)です。
色指定に関して
Desmumeを含むほとんどのエミュレータでは80red glitchと言って0x7FFFFFFFF以上の値を指定すると描画が行われなくなるバグがあります。色の指定時には注意が必要です。
これでLua Scriptを動かすことができるようになりました。次回以降はDesmumeでLua Scriptを活用するための基礎知識、Desmumeに登録されているAPIの説明を行います。
-
wikipedia Lua 1994年かも?ライセンスは1994~
https://ja.wikipedia.org/wiki/Lua (2024/01/29 アクセス) ↩ -
DesmumeはC++で動いているソフト ↩
-
Aviutlは32bitソフトなので入っているlua51.dllは32bit版である場合がほとんどです。動かなかったら大人しく手順1のとおりにダウンロードしてください。 ↩
-
もし今ROMを用意していてROMを読み込んだ状態でluaを動かせるのであれば1番目と2番目をもう一度比べてみてください。1番目のほうでは奇妙なことが起こるでしょう。エミュレートされた画面が更新されないのはDesmumeが抱えるバグであり2番目を使わなければならない理由の一つです。 ↩
-
こちらも頑張って書きます。書いたらここに記事のURLが来る。 ↩