Remote control emulator Fceux menggunakan Python

Dalam artikel ini, saya akan menjelaskan cara membuat emulator NES dikendalikan dari jarak jauh, dan server untuk mengirim perintah ke sana dari jarak jauh.



Mengapa ini dibutuhkan?


Beberapa emulator dari berbagai konsol game, termasuk Fceux , memungkinkan Anda untuk menulis dan menjalankan skrip khusus di Lua. Tapi Lua adalah bahasa yang buruk untuk menulis program serius. Ini lebih merupakan bahasa untuk fungsi panggilan yang ditulis dalam C. Para penulis emulator menggunakannya hanya karena ringan dan mudah ditanamkan. Emulasi yang akurat membutuhkan banyak sumber daya prosesor, dan kecepatan emulasi sebelumnya adalah salah satu tujuan utama penulis, dan jika mereka mengingat kemungkinan tindakan scripting, itu tidak di tempat pertama.

Sekarang kekuatan prosesor rata-rata sudah cukup untuk meniru NES, mengapa tidak menggunakan bahasa skrip yang kuat seperti Python atau JavaScript dalam emulator?

Sayangnya, tidak ada emulator NES populer yang memiliki kemampuan untuk menggunakan ini atau bahasa lain. Saya hanya menemukan proyek Nintaco yang sedikit diketahui, yang juga didasarkan pada kernel Fceux, untuk beberapa alasan ditulis ulang di Jawa. Kemudian saya memutuskan untuk menambahkan kemampuan menulis skrip dengan Python untuk mengendalikan emulator sendiri.

Hasil saya adalah Proof-of-Concept dari kemampuan untuk mengendalikan emulator, ia tidak berpura-pura kecepatan atau keandalan, tetapi ia bekerja. Saya melakukannya untuk diri saya sendiri, tetapi karena pertanyaan tentang bagaimana mengendalikan emulator menggunakan skrip cukup umum , saya meletakkan kode sumber pada github .

Bagaimana cara kerjanya


Di sisi emulator


Emulator Fceux sudah menyertakan beberapa perpustakaan Lua yang disertakan di dalamnya dalam bentuk kode yang dikompilasi . Salah satunya adalah LuaSocket . Ini didokumentasikan dengan buruk, tetapi saya berhasil menemukan contoh kode kerja di antara kumpulan skrip Xkeeper0 . Dia menggunakan soket untuk mengontrol emulator melalui Mirc. Sebenarnya, kode yang membuka soket tcp adalah:

function connect(address, port, laddress, lport) local sock, err = socket.tcp() if not sock then return nil, err end if laddress then local res, err = sock:bind(laddress, lport, -1) if not res then return nil, err end end local res, err = sock:connect(address, port) if not res then return nil, err end return sock end sock2, err2 = connect("127.0.0.1", 81) sock2:settimeout(0) --it's our socket object print("Connected", sock2, err2) 

Ini adalah soket tingkat rendah yang menerima dan mengirim data sebesar 1 byte.

Dalam emulator Fceux, loop utama skrip Lua terlihat seperti ini:

 function main() while true do --  passiveUpdate() --,        emu.frameadvance() --       end end 

Pemeriksaan data dari soket:

 function passiveUpdate() local message, err, part = sock2:receive("*all") if not message then message = part end if message and string.len(message)>0 then --print(message) local recCommand = json.decode(message) table.insert(commandsQueue, recCommand) coroutine.resume(parseCommandCoroutine) end end 

Kode ini cukup sederhana - data dibaca dari soket, dan jika perintah berikutnya terdeteksi, maka diuraikan dan dieksekusi. Parsing dan eksekusi diatur menggunakan coroutine (coroutine) - ini adalah konsep yang kuat dari bahasa Lua untuk menjeda dan melanjutkan eksekusi kode.

Dan satu hal lagi yang penting tentang skrip Lua di Fceux - emulasi dapat dihentikan sementara dari skrip. Bagaimana mengatur kelanjutan eksekusi kode Lua dan menjalankannya kembali dengan perintah yang diterima dari soket? Ini tidak mungkin, tetapi ada kemampuan yang tidak terdokumentasi dengan baik untuk memanggil kode Lua bahkan ketika emulasi dihentikan (terima kasih karena telah menunjukkannya ):

 gui.register(passiveUpdate) --undocumented. this function will call even if emulator paused 

Dengannya , Anda dapat berhenti dan terus mengemulasi di dalam pembaruan pasif - dengan cara ini Anda dapat mengatur pemasangan breakpoint emulator melalui soket.

Perintah sisi server


Saya menggunakan protokol teks RPC berbasis JSON yang sangat sederhana. Server membuat serial nama fungsi dan argumen menjadi string JSON dan mengirimkannya melalui soket. Lebih lanjut, eksekusi kode dihentikan sampai emulator merespons dengan garis untuk menyelesaikan perintah. Respons akan berisi bidang " FUNCTIONNAME_finished " dan hasil dari fungsinya.

Idenya diimplementasikan di kelas syncCall :

 class syncCall: @classmethod def waitUntil(cls, messageName): """cycle for reading data from socket until needed message was read from it. All other messages will added in message queue""" while True: cmd = messages.parseMessages(asyncCall.waitAnswer(), [messageName]) #print(cmd) if cmd != None: if len(cmd)>1: return cmd[1] return @classmethod def call(cls, *params): """wrapper for sending [functionName, [param1, param2, ...]] to socket and wait until client return [functionName_finished, [result1,...]] answer""" sender.send(*params) funcName = params[0] return syncCall.waitUntil(funcName + "_finished") 

Dengan kelas ini, metode Lua emulator Fceux dapat dibungkus dalam kelas Python:

 class emu: @classmethod def poweron(cls): return syncCall.call("emu.poweron") @classmethod def pause(cls): return syncCall.call("emu.pause") @classmethod def unpause(cls): return syncCall.call("emu.unpause") @classmethod def message(cls, str): return syncCall.call("emu.message", str) @classmethod def softreset(cls): return syncCall.call("emu.softreset") @classmethod def speedmode(cls, str): return syncCall.call("emu.speedmode", str) 

Dan kemudian memanggil kata demi kata dengan cara yang sama seperti dari Lua:

 # : emu.poweron() 

Metode panggilan balik


Di Lua, Anda dapat mendaftarkan panggilan balik - fungsi yang akan dipanggil saat kondisi tertentu terpenuhi. Kita bisa port perilaku ini ke server di Python menggunakan trik berikut. Pertama, kita menyimpan pengenal fungsi callback yang ditulis dengan Python dan meneruskannya ke kode Lua:

 class callbacks: functions = {} callbackList = [ "emu.registerbefore_callback", "emu.registerafter_callback", "memory.registerexecute_callback", "memory.registerwrite_callback", ] @classmethod def registerfunction(cls, func): if func == None: return 0 hfunc = hash(func) callbacks.functions[hfunc] = func return hfunc @classmethod def error(cls, e): emu.message("Python error: " + str(e)) @classmethod def checkAllCallbacks(cls, cmd): #print("check:", cmd) for callbackName in callbacks.callbackList: if cmd[0] == callbackName: hfunc = cmd[1] #print("hfunc:", hfunc) func = callbacks.functions.get(hfunc) #print("func:", func) if func: try: func(*cmd[2:]) #skip function name and function hash and save others arguments except Exception as e: callbacks.error(e) pass #TODO: thread locking sender.send(callbackName + "_finished") 

Lua-code juga menyimpan pengenal ini dan mendaftarkan Lua-callback biasa, yang akan mentransfer kontrol ke Python-code. Selanjutnya, utas terpisah dibuat dalam kode Python yang hanya berkaitan dengan memeriksa bahwa perintah panggilan balik dari Lua belum diterima:

 def callbacksThread(): cycle = 0 while True: cycle += 1 try: cmd = messages.parseMessages(asyncCall.waitAnswer(), callbacks.callbackList) if cmd: #print("Callback received:", cmd) callbacks.checkAllCallbacks(cmd) pass except socket.timeout: pass time.sleep(0.001) 

Langkah terakhir adalah bahwa setelah panggilan balik Python dieksekusi, kontrol dikembalikan ke Lua menggunakan perintah " CALLBACKNAME_finished " untuk memberi tahu emulator bahwa panggilan balik telah selesai.

Cara menjalankan contoh


  • Anda harus menjalankan Python 3 dan Jupyter Notebook di sistem. Anda harus menjalankan Jupyter dengan perintah

     jupyter notebook 

  • Buka laptop FceuxPythonServer.py.ipynb dan jalankan baris pertama

  • Sekarang Anda perlu menjalankan emulator Fceux, buka file ROM di dalamnya (saya menggunakan game Castlevania (U) (PRG0) [!]. Nes dalam contoh saya) dan jalankan skrip Lua dengan nama fceux_listener.lua . Seharusnya terhubung ke server yang berjalan di laptop Jupyter.

    Tindakan ini dapat dilakukan menggunakan baris perintah:

     fceux.exe -lua fceux_listener.lua "Castlevania (U) (PRG0) [!].nes" 

  • Sekarang kembali ke Notebook Jupyter. Anda akan melihat pesan tentang koneksi yang berhasil ke emulator:



Itu saja, Anda dapat mengirim perintah dari laptop Jupyter di browser langsung ke emulator Fceux.

Anda dapat menjalankan semua baris laptop contoh secara berurutan dan mengamati hasil eksekusi di emulator.

Contoh lengkap:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynb

Ini berisi fungsi sederhana seperti membaca memori:



Contoh panggilan balik yang lebih kompleks:



Dan skrip untuk game tertentu yang memungkinkan Anda untuk memindahkan musuh dari Super Mario Bros. dengan mouse:



Video Jalankan Laptop:


Keterbatasan dan Aplikasi


Script tidak memiliki perlindungan terhadap kebodohan dan tidak dioptimalkan untuk kecepatan eksekusi - akan lebih baik menggunakan protokol RPC biner daripada teks satu dan pesan grup bersama, tetapi implementasi saya tidak memerlukan kompilasi. Script dapat mengubah konteks eksekusi dari Lua ke Python dan kembali 500-1000 kali per detik di laptop saya. Ini cukup untuk hampir semua aplikasi, kecuali untuk kasus-kasus tertentu dari pixel-by-pixel atau debugging baris-demi-baris dari prosesor video, tetapi Fceux masih tidak mengizinkan operasi semacam itu dari Lua, jadi itu tidak masalah.

Gagasan aplikasi yang mungkin:

  • Sebagai contoh penerapan kontrol tersebut untuk emulator dan bahasa lainnya
  • Penelitian game
  • Menambahkan cheat atau fitur untuk mengatur bagian TAS
  • Masukkan atau ekstrak data dan kode ke dalam game
  • Meningkatkan kemampuan emulator - menulis debugger, skrip untuk merekam dan melihat langkah-langkah, perpustakaan skrip, editor game
  • Game jaringan, kontrol game menggunakan perangkat seluler, layanan jarak jauh, joypad atau perangkat kontrol lainnya, menyimpan dan menambal layanan cloud
  • Fitur Cross-Emulator
  • Menggunakan Python atau perpustakaan bahasa lain untuk analisis data dan kontrol game (membuat bot)

Tumpukan teknologi


Saya menggunakan:

Fceux - www.fceux.com/web/home.html
Ini adalah emulator NES klasik, dan kebanyakan orang menggunakannya. Ini belum diperbarui untuk waktu yang lama, dan bukan yang terbaik dalam fitur, tetapi tetap emulator default untuk banyak romhackers. Juga, saya memilihnya karena dukungan soket Lua terintegrasi ke dalamnya, dan tidak perlu menghubungkannya sendiri.

Json.lua - github.com/spiiin/json.lua
Ini adalah implementasi JSON di Lua murni. Saya memilihnya karena saya ingin membuat contoh yang tidak memerlukan kompilasi kode. Tetapi saya masih harus mem-fork perpustakaan, karena beberapa perpustakaan yang dibangun di Fceux membebani fungsi tostring perpustakaan dan memutus serialisasi ( permintaan pool saya yang ditolak kepada penulis perpustakaan asli).

Python 3 - www.python.org
Server Fceux Lua membuka soket tcp dan mendengarkan perintah yang diterima darinya. Server yang mengirimkan perintah ke emulator dapat diimplementasikan dalam bahasa apa pun. Saya memilih Python karena filosofi "Termasuk baterai" - sebagian besar modul termasuk dalam perpustakaan standar (bekerja dengan soket dan JSON juga). Python juga tahu perpustakaan untuk bekerja dengan jaringan saraf, dan saya ingin mencoba menggunakannya untuk membuat bot di game NES.

Jupyter Notebook - jupyter.org
Jupyter Notebook adalah lingkungan yang sangat keren untuk mengeksekusi kode Python secara interaktif. Dengannya, Anda dapat menulis dan menjalankan perintah dalam editor spreadsheet di dalam browser. Ini juga bagus untuk membuat contoh yang bagus.

Dexpot - www.dexpot.de
Saya menggunakan manajer desktop virtual ini untuk merapat jendela emulator di atas yang lain. Ini sangat nyaman ketika menempatkan server dalam layar penuh untuk pelacakan instan perubahan di jendela emulator. Alat Windows asli tidak memungkinkan Anda untuk mengatur docking windows di atas yang lain.

Referensi


Sebenarnya, repositori proyek .

Nintaco - Java NES Emulator dengan Remote Management
Xkeeper0 emu-lua collection - koleksi berbagai skrip Lua
Mesen adalah emulator NES modern di C # dengan kemampuan skrip Lua yang kuat. Sejauh ini tanpa dukungan soket dan remote control.
CadEditor adalah proyek saya editor tingkat universal untuk NES dan platform lainnya, serta alat yang kuat untuk meneliti game. Saya menggunakan skrip dan server yang dijelaskan dalam posting untuk menjelajahi permainan dan menambahkannya ke editor.

Saya akan menghargai umpan balik, pengujian, dan upaya untuk menggunakan skrip.

Source: https://habr.com/ru/post/id435428/


All Articles