جهاز التحكم عن بعد لمحاكي Fceux باستخدام Python

في المقالة ، سوف أصف كيفية التحكم في محاكي NES عن بُعد ، وخادم لإرسال الأوامر إليه عن بُعد.



لماذا هذا مطلوب؟


تتيح لك بعض برامج محاكاة الألعاب المختلفة ، بما في ذلك Fceux ، كتابة وتشغيل برامج نصية مخصصة على Lua. لكن لوا هي لغة سيئة لكتابة برامج جادة. هي بالأحرى لغة للاتصال بالوظائف المكتوبة باللغة C. يستخدمه مؤلفو المحاكاة فقط بسبب خفة وسهولة التضمين. يتطلب المحاكاة الدقيقة الكثير من موارد المعالج ، وكانت سرعة المضاهاة السابقة أحد الأهداف الرئيسية للمؤلفين ، وإذا تذكروا إمكانية إجراء عمليات البرمجة ، فلن يكون ذلك في المقام الأول.

الآن قوة المعالج المتوسط ​​كافية لمحاكاة NES ، لماذا لا تستخدم لغات البرمجة النصية القوية مثل Python أو JavaScript في برامج المحاكاة؟

لسوء الحظ ، ليس لدى أي من برامج محاكاة NES الشائعة القدرة على استخدام هذه اللغات أو اللغات الأخرى. لقد وجدت فقط مشروع Nintaco غير معروف ، والذي يستند أيضًا إلى Fceux kernel ، لسبب ما تمت إعادة كتابته في Java. ثم قررت إضافة القدرة على كتابة البرامج النصية في بيثون للسيطرة على المحاكي بنفسي.

النتيجة هي إثبات القدرة على التحكم في المحاكي ، فهي لا تتظاهر بالسرعة أو الموثوقية ، ولكنها تعمل. لقد فعلت ذلك بنفسي ، ولكن نظرًا لأن مسألة كيفية التحكم في المحاكي باستخدام البرامج النصية أمر شائع بما فيه الكفاية ، فقد وضعت الكود المصدري على جيثب .

كيف يعمل؟


على جانب المحاكي


يحتوي المحاكي Fceux بالفعل على العديد من مكتبات Lua المضمنة فيه في شكل تعليمات برمجية برمجية . واحد منهم هو LuaSocket . تم توثيقه بشكل سيئ ، لكنني تمكنت من العثور على مثال لرمز العمل بين مجموعة البرامج النصية Xkeeper0 . اعتاد مآخذ للسيطرة على المضاهاة من خلال Mirc. في الواقع ، الكود الذي يفتح مقبس tcp هو:

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) 

هذا مقبس منخفض المستوى يستقبل ويرسل البيانات بمقدار 1 بايت.

في محاكي Fceux ، تبدو الحلقة الرئيسية في نص Lua كما يلي:

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

فحص البيانات من المقبس:

 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 

الرمز بسيط جدًا - تتم قراءة البيانات من المقبس ، وإذا تم اكتشاف الأمر التالي ، فسيتم تحليله وتنفيذه. يتم تنظيم التحليل والتنفيذ باستخدام coroutine (coroutines) - وهذا مفهوم قوي للغة Lua للتوقف المؤقت وتنفيذ التعليمات البرمجية المستمرة.

وهناك شيء آخر مهم حول برمجة Lua في Fceux - محاكاة يمكن إيقافها مؤقتًا من البرنامج النصي. كيفية تنظيم التنفيذ المستمر لرمز Lua وإعادة تشغيله باستخدام أمر تم استلامه من المقبس؟ لن يكون ذلك ممكنًا ، ولكن هناك قدرة موثقة بشكل سيء على استدعاء رمز Lua حتى عند إيقاف المحاكاة (شكرًا لك على الإشارة إليه):

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

مع ذلك ، يمكنك إيقاف ومتابعة مضاهاة داخل passiveUpdate - بهذه الطريقة يمكنك تنظيم تثبيت نقاط التوقف الخاصة بالمضاهاة عبر مأخذ توصيل.

أمر جانب الخادم


أستخدم بروتوكول نص RPC بسيط جدًا يستند إلى JSON. يقوم الخادم بتسلسل اسم الوظيفة والوسيطات في سلسلة JSON وإرسالها عبر المقبس. علاوة على ذلك ، يتوقف تنفيذ التعليمات البرمجية حتى يستجيب المحاكي بسطر لإكمال تنفيذ الأمر. ستتضمن الاستجابة الحقول " FUNCTIONNAME_finished " ونتائج الوظيفة.

يتم تطبيق الفكرة في فئة 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") 

باستخدام هذه الفئة ، يمكن لف أساليب المحاكي Fceux Lua في فئات 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) 

ثم دعا حرفيا بنفس الطريقة كما في لوا:

 # : emu.poweron() 

طرق رد الاتصال


في Lua ، يمكنك تسجيل عمليات الاسترجاعات - الوظائف التي سيتم استدعاؤها عند تلبية شرط معين. يمكننا نقل هذا السلوك إلى الخادم في Python باستخدام الخدعة التالية. أولاً ، نحفظ معرف وظيفة رد الاتصال المكتوبة في Python ونمررها إلى رمز 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 أيضًا هذا المعرّف ويسجل رد اتصال Lua-call منتظم ، والذي سينقل التحكم إلى كود Python. بعد ذلك ، يتم إنشاء سلسلة رسائل منفصلة في شفرة Python التي تهتم فقط بالتحقق من عدم قبول أمر رد الاتصال من Lua:

 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) 

الخطوة الأخيرة هي أنه بعد تنفيذ رد الاتصال Python ، يتم إرجاع التحكم إلى Lua باستخدام الأمر " CALLBACKNAME_finished " لإبلاغ المحاكي بإنهاء رد الاتصال.

كيفية تشغيل مثال


  • يجب أن يكون لديك Python 3 و Jupyter Notebook على النظام. يجب عليك تشغيل Jupyter مع الأمر

     jupyter notebook 

  • افتح الكمبيوتر المحمول FceuxPythonServer.py.ipynb وقم بتشغيل السطر الأول

  • الآن تحتاج إلى تشغيل محاكي Fceux ، افتح ملف ROM به (يمكنني استخدام لعبة Castlevania (U) (PRG0) [!]. Nes في مثالي) وتشغيل البرنامج النصي Lua باسم fceux_listener.lua . يجب أن تتصل بخادم يعمل على كمبيوتر محمول Jupyter.

    يمكن تنفيذ هذه الإجراءات باستخدام سطر الأوامر:

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

  • انتقل الآن مرة أخرى إلى Jupyter Notebook. يجب أن تشاهد رسالة حول اتصال ناجح مع المحاكي:



هذا كل شيء ، يمكنك إرسال أوامر من الكمبيوتر المحمول Jupyter في المستعرض مباشرة إلى محاكي Fceux.

يمكنك تنفيذ جميع أسطر الكمبيوتر المحمول المثال بالتتابع ومراقبة نتيجة التنفيذ في المحاكي.

مثال كامل:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynb

يحتوي على وظائف بسيطة مثل قراءة الذاكرة:



أمثلة رد الاتصال أكثر تعقيدًا:



ونصًا لعبة معينة تسمح لك بنقل الأعداء من Super Mario Bros. مع الماوس:



تشغيل الفيديو المحمول:


القيود والتطبيقات


لا يحتوي البرنامج النصي على أي حماية ضد الخداع ولم يتم تحسينه لسرعة التنفيذ - سيكون من الأفضل استخدام بروتوكول RPC ثنائي بدلاً من نص واحد وتجميع الرسائل معًا ، لكن تطبيقي لا يتطلب تجميعًا. يمكن للبرنامج النصي تبديل سياقات التنفيذ من Lua إلى Python وإعادة 500-1000 مرة في الثانية على جهاز الكمبيوتر المحمول. هذا يكفي لأي تطبيق تقريبًا ، باستثناء حالات محددة من تصحيح البيكسل أو بكسل لسطر معالج الفيديو ، لكن Fceux لا يزال لا يسمح بمثل هذه العمليات من Lua ، لذلك لا يهم.

أفكار التطبيق الممكنة:

  • كمثال على تنفيذ هذه الرقابة لغيرها من المحاكيات واللغات
  • لعبة البحث
  • إضافة غش أو ميزات لتنظيم مقاطع TAS
  • إدراج أو استخراج البيانات والرمز في الألعاب
  • تعزيز قدرات المحاكيات - كتابة مصححات الأخطاء والبرامج النصية لتسجيل وعرض الإرشادات التفصيلية ، مكتبات النصوص ، محرري الألعاب
  • لعبة الشبكة ، والتحكم في اللعبة باستخدام الأجهزة المحمولة ، والخدمات عن بعد ، joypads أو غيرها من أجهزة التحكم ، وتوفير والبقع في الخدمات السحابية
  • عبر محاكي الميزات
  • استخدام Python أو مكتبات اللغات الأخرى لتحليل البيانات والتحكم في اللعبة (إنشاء برامج روبوتات)

كومة التكنولوجيا


اعتدت:

Fceux - www.fceux.com/web/home.html
هذا محاكي NES كلاسيكي ، ويستخدمه معظم الناس. لم يتم تحديثه لفترة طويلة ، وليس الأفضل في الميزات ، لكنه يظل محاكيًا افتراضيًا للعديد من برامج قراءة الرموز. أيضًا ، لقد اخترتها لأن دعم مأخذ التوصيل Lua مدمج فيه ، وليست هناك حاجة لتوصيله بنفسي.

Json.lua - github.com/spiiin/json.lua
هذا هو تنفيذ JSON في لوا النقي. لقد اخترت ذلك لأنني أردت أن أقدم مثالًا لا يتطلب تجميع التعليمات البرمجية. لكنني ما زلت مضطرًا لتفرع المكتبة ، لأن بعض المكتبات المضمّنة في Fceux أثقلت وظيفة المكتبة على التتابع وكسرت التسلسل ( طلب التجميع المرفوض إلى مؤلف المكتبة الأصلية).

بيثون 3 - www.python.org
يفتح خادم Fceux Lua مقبس tcp ويستمع للأوامر الواردة منه. يمكن تنفيذ خادم يرسل أوامر إلى المحاكي بأي لغة. لقد اخترت Python لفلسفتها "Battery included" - يتم تضمين معظم الوحدات في المكتبة القياسية (تعمل مع مآخذ و JSON أيضًا). يعرف Python أيضًا المكتبة للعمل مع الشبكات العصبية ، وأريد أن أحاول استخدامها لإنشاء روبوتات في ألعاب NES.

Jupyter Notebook - jupyter.org
Jupyter Notebook هو بيئة رائعة جدًا لتنفيذ كود Python بشكل تفاعلي. مع ذلك ، يمكنك كتابة وتنفيذ الأوامر في محرر جدول بيانات داخل المتصفح. إنه جيد أيضًا لإنشاء أمثلة رائعة.

Dexpot - www.dexpot.de
لقد استخدمت مدير سطح المكتب الافتراضي هذا لإرساء نافذة المحاكي أعلى الآخرين. هذا مناسب جدًا عند نشر الخادم بملء الشاشة للتتبع الفوري للتغييرات في نافذة المحاكي. لا تسمح لك أدوات Windows الأصلية بتنظيم إرساء النوافذ أعلى الآخرين.

المراجع


في الواقع ، مستودع المشروع .

Nintaco - Java NES Emulator مع الإدارة عن بعد
مجموعة Xkeeper0 emu-lua - مجموعة من نصوص Lua المختلفة
Mesen هو محاكي NES الحديث في C # مع قدرات البرمجة النصية قوية لوا. حتى الآن دون دعم المقبس والتحكم عن بعد.
CadEditor هو مشروعي لمحرر المستوى العالمي لـ NES والأنظمة الأساسية الأخرى ، بالإضافة إلى أدوات قوية للبحث في الألعاب. أستخدم البرنامج النصي والخادم الموصوفين في المنشور لاستكشاف الألعاب وإضافتها إلى المحرر.

سأكون ممتنًا للتعليقات والاختبارات ومحاولات استخدام البرنامج النصي.

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


All Articles