Dans l'article, je décrirai comment rendre l'émulateur NES contrôlé à distance, et un serveur pour lui envoyer des commandes à distance.

Pourquoi est-ce nécessaire?
Certains émulateurs de différentes consoles de jeux, dont
Fceux , vous permettent d'écrire et d'exécuter des scripts personnalisés sur Lua. Mais Lua est un mauvais langage pour écrire des programmes sérieux. C'est plutôt un langage pour appeler des fonctions écrites en C. Les auteurs d'émulateurs l'utilisent uniquement en raison de la légèreté et de la facilité d'intégration. Une émulation précise nécessite beaucoup de ressources processeur, et une vitesse d'émulation antérieure était l'un des principaux objectifs des auteurs, et s'ils se souvenaient de la possibilité d'actions de script, ce n'était pas le premier.
Maintenant, la puissance du processeur moyen suffit pour émuler NES, pourquoi ne pas utiliser des langages de script puissants comme Python ou JavaScript dans les émulateurs?
Malheureusement, aucun des émulateurs NES populaires n'a la capacité d'utiliser ces langues ou d'autres. Je n'ai trouvé qu'un projet
Nintaco peu connu, qui est également basé sur le noyau Fceux, pour une raison quelconque réécrit en Java. J'ai ensuite décidé d'ajouter la possibilité d'écrire des scripts en Python pour contrôler moi-même l'émulateur.
Mon résultat est le Proof-of-Concept de la capacité à contrôler l'émulateur, il ne prétend ni vitesse ni fiabilité, mais ça marche. Je l'ai fait moi-même, mais comme la question de savoir comment contrôler l'émulateur à l'aide de scripts est
assez courante , j'ai mis le code source sur le
github .
Comment ça marche
Du côté de l'émulateur
L'émulateur Fceux
inclut déjà plusieurs bibliothèques Lua incluses sous
forme de code compilé . L'un d'eux est
LuaSocket . C'est mal documenté, mais j'ai réussi à trouver un exemple de code de travail parmi la
collection de scripts Xkeeper0 . Il a utilisé des prises pour contrôler l'émulateur via Mirc. En fait, le code qui ouvre le socket TCP est:
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)
Il s'agit d'un socket de bas niveau qui reçoit et envoie des données d'un octet.
Dans l'émulateur Fceux, la boucle principale du script Lua ressemble à ceci:
function main() while true do
Une vérification des données du socket:
function passiveUpdate() local message, err, part = sock2:receive("*all") if not message then message = part end if message and string.len(message)>0 then
Le code est assez simple - les données sont lues à partir du socket, et si la prochaine commande est détectée, elle est analysée et exécutée. L'analyse et l'exécution sont organisées à l'aide de
coroutine (coroutines) - il s'agit d'un concept puissant du langage Lua pour suspendre et poursuivre l'exécution de code.
Et une autre chose importante à propos des scripts Lua dans Fceux - l'émulation peut être temporairement arrêtée à partir du script. Comment organiser l'exécution continue du Lua-code et le réexécuter avec une commande reçue du socket? Ce ne serait pas possible, mais il existe une capacité mal documentée d'appeler du code Lua même lorsque l'émulation est arrêtée (merci
feos de l' avoir
pointé ):
gui.register(passiveUpdate)
Avec lui, vous pouvez arrêter et continuer d'émuler à l'intérieur de
passiveUpdate - de cette façon, vous pouvez organiser l'installation des points d'arrêt de l'émulateur via une socket.
Commande côté serveur
J'utilise un protocole texte RPC basé sur JSON très simple. Le serveur sérialise le nom de la fonction et les arguments dans une chaîne JSON et les envoie via le socket. En outre, l'exécution du code s'arrête jusqu'à ce que l'émulateur réponde par une ligne pour terminer l'exécution de la commande. La réponse contiendra les champs "
FUNCTIONNAME_finished " et le résultat de la fonction.
L'idée est implémentée dans la classe
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])
Avec cette classe, les méthodes Lua de l'émulateur Fceux peuvent être enveloppées dans des classes 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)
Et puis appelé mot pour mot de la même manière que de Lua:
Méthodes de rappel
Dans Lua, vous pouvez enregistrer des rappels - des fonctions qui seront appelées lorsqu'une certaine condition est remplie. Nous pouvons porter ce comportement sur le serveur en Python en utilisant l'astuce suivante. Tout d'abord, nous enregistrons l'identifiant de la fonction de rappel écrit en Python et le transmettons au code 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):
Le code Lua enregistre également cet identifiant et enregistre un rappel Lua régulier, qui transférera le contrôle au code Python. Ensuite, un thread séparé est créé dans le code Python uniquement pour vérifier que la commande de rappel de Lua n'a pas été acceptée:
def callbacksThread(): cycle = 0 while True: cycle += 1 try: cmd = messages.parseMessages(asyncCall.waitAnswer(), callbacks.callbackList) if cmd:
La dernière étape est qu'après l'exécution du rappel Python, le contrôle est retourné à Lua à l'aide de la
commande "
CALLBACKNAME_finished " pour informer l'émulateur que le rappel est terminé.
Comment exécuter un exemple
C'est tout, vous pouvez envoyer des commandes de l'ordinateur portable Jupyter dans le navigateur directement à l'émulateur Fceux.
Vous pouvez exécuter toutes les lignes de l'exemple d'ordinateur portable de manière séquentielle et observer le résultat dans l'émulateur.
Exemple complet:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynbIl contient des fonctions simples comme lire la mémoire:

Exemples de rappel plus complexes:

Et un script pour un jeu spécifique qui vous permet de déplacer les ennemis de
Super Mario Bros. avec la souris:

Vidéo sur l'ordinateur portable:
Limitations et applications
Le script n'a aucune protection contre les imbéciles et n'est pas optimisé pour la vitesse d'exécution - il serait préférable d'utiliser un protocole RPC binaire au lieu d'un texte et de regrouper les messages, mais mon implémentation ne nécessite pas de compilation. Le script peut basculer les contextes d'exécution de Lua à Python et revenir 500 à 1000 fois par seconde sur mon ordinateur portable. Cela suffit pour presque toutes les applications, à l'exception de cas spécifiques de débogage pixel par pixel ou ligne par ligne du processeur vidéo, mais Fceux n'autorise toujours pas de telles opérations à partir de Lua, donc cela n'a pas d'importance.
Idées d'application possibles:
- Comme exemple de mise en œuvre d'un tel contrôle pour d'autres émulateurs et langages
- Recherche de jeux
- Ajout de tricheurs ou de fonctionnalités pour organiser les passages TAS
- Insérer ou extraire des données et du code dans les jeux
- Amélioration des capacités des émulateurs - écriture de débogueurs, de scripts pour l'enregistrement et la visualisation de procédures pas à pas, bibliothèques de scripts, éditeurs de jeux
- Jeu en réseau, contrôle de jeu à l'aide d'appareils mobiles, de services à distance, de manettes de jeu ou d'autres appareils de contrôle, enregistrement et correctifs dans les services cloud
- Fonctionnalités Cross-Emulator
- Utilisation de Python ou d'autres bibliothèques de langage pour l'analyse des données et le contrôle du jeu (création de bots)
Pile technologique
J'ai utilisé:
Fceux -
www.fceux.com/web/home.htmlIl s'agit d'un émulateur NES classique et la plupart des gens l'utilisent. Il n'a pas été mis à jour depuis longtemps et n'est pas le meilleur des fonctionnalités, mais il reste l'émulateur par défaut de nombreux romhackers. Aussi, je l'ai choisi car le support de socket Lua y est intégré, et il n'est pas nécessaire de le connecter moi-même.
Json.lua -
github.com/spiiin/json.luaIl s'agit d'une implémentation JSON en pur Lua. Je l'ai choisi parce que je voulais faire un exemple qui ne nécessite pas de compilation de code. Mais j'ai quand même dû bifurquer la bibliothèque, car certaines des bibliothèques intégrées à Fceux ont surchargé la fonction de bibliothèque
tostring et ont interrompu la sérialisation (ma
demande de pool rejetée à l'auteur de la bibliothèque d'origine).
Python 3 -
www.python.orgLe serveur Fceux Lua ouvre le socket tcp et écoute les commandes reçues de celui-ci. Un serveur qui envoie des commandes à l'émulateur peut être implémenté dans n'importe quelle langue. J'ai choisi Python pour sa philosophie «Batterie incluse» - la plupart des modules sont inclus dans la bibliothèque standard (fonctionnant également avec les sockets et JSON). Python connaît également la bibliothèque pour travailler avec les réseaux de neurones, et je veux essayer de les utiliser pour créer des bots dans les jeux NES.
Carnet Jupyter -
jupyter.orgJupyter Notebook est un environnement très cool pour exécuter de manière interactive du code Python. Avec lui, vous pouvez écrire et exécuter des commandes dans un éditeur de feuille de calcul à l'intérieur du navigateur. Il est également bon pour créer des exemples présentables.
Dexpot -
www.dexpot.deJ'ai utilisé ce gestionnaire de bureau virtuel pour ancrer la fenêtre de l'émulateur au-dessus des autres. Ceci est très pratique lors du déploiement du serveur en plein écran pour un suivi instantané des modifications dans la fenêtre de l'émulateur. Les outils Windows natifs ne vous permettent pas d'organiser l'ancrage des fenêtres au-dessus des autres.
Les références
En fait,
le référentiel du projet .
Nintaco - Émulateur Java NES avec gestion à distance
Collection emke-lua Xkeeper0 - une collection de divers scripts Lua
Mesen est un émulateur NES moderne en C # avec de puissantes capacités de script Lua. Jusqu'à présent, sans prise en charge de prise et télécommande.
CadEditor est mon projet d'un éditeur de niveau universel pour NES et d'autres plateformes, ainsi que de puissants outils de recherche de jeux. J'utilise le script et le serveur décrits dans l'article pour explorer les jeux et les ajouter à l'éditeur.
J'apprécierais les commentaires, les tests et les tentatives d'utilisation du script.