使用Python远程控制Fceux仿真器

在本文中,我将介绍如何远程控制NES仿真器,以及如何向其远程发送命令的服务器。



为什么需要这个?


包括Fceux在内的各种游戏机的一些仿真器,使您可以在Lua上编写和运行自定义脚本。 但是Lua对于编写严肃的程序是一种不好的语言。 而是一种用于调用用C编写的函数的语言。 仿真器的作者仅由于其轻巧和易于嵌入而使用它。 准确的仿真需要大量的处理器资源,而更早的仿真速度是作者的主要目标之一,如果他们记得脚本操作的可能性,那么它就不是第一位的。

现在,普通处理器的功能足以模拟NES,为什么不在模拟器中使用功能强大的脚本语言(如Python或JavaScript)?

不幸的是,没有一种流行的NES仿真器能够使用这些或其他语言。 我发现只有一个鲜为人知的Nintaco项目,该项目也基于Fceux内核,出于某种原因用Java重写了。 然后,我决定增加使用Python编写脚本来自己控制模拟器的功能。

我的结果是控制仿真器的能力的概念验证,它不假装速度或可靠性,但可以正常工作。 我自己完成了此操作,但是由于如何使用脚本控制模拟器的问题很常见 ,因此将源代码放在github上

如何运作


在仿真器的侧面


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 

代码非常简单-从套接字读取数据,如果检测到下一个命令,则将对其进行解析和执行。 解析和执行是使用程组织的(协程)-这是Lua语言用于暂停和继续执行代码的强大概念。

关于Fceux中的Lua脚本编写的另一件重要事情-可以从脚本中暂时停止仿真。 如何组织Lua代码的继续执行并使用从套接字收到的命令重新运行它? 这是不可能的,但是即使在仿真停止时,调用Lua代码的能力也很差(感谢专人指出 ):

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

有了它,您可以停止并继续在passiveUpdate内部进行仿真-这样,您可以通过套接字来组织仿真器的断点的安装。

服务器端命令


我使用一个非常简单的基于JSON的RPC文本协议。 服务器将函数名称和参数序列化为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) 

然后以与Lua相同的方式调用逐字记录:

 # : 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代码还会保存此标识符并注册常规的Lua回调,这会将控制权转移到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回调后,使用“ CALLBACKNAME_finished命令将控制权返回给Lua,以通知仿真器该回调已完成。

如何运行一个例子


  • 您必须在系统上运行Python 3Jupyter Notebook 。 您必须使用以下命令运行Jupyter

     jupyter notebook 

  • 打开FceuxPythonServer.py.ipynb便携式计算机并运行第一行

  • 现在,您需要运行Fceux仿真器,打开其中的ROM文件(在我的示例中,我使用游戏Castlevania(U)(PRG0)[!]。Nes )并运行名称为fceux_listener.lua的Lua脚本。 它应该连接到在Jupyter笔记本电脑上运行的服务器。

    可以使用命令行执行以下操作:

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

  • 现在切换回Jupyter笔记本。 您应该看到有关成功连接到模拟器的消息:



就是这样,您可以从浏览器中的Jupyter膝上型计算机将命令直接发送到Fceux仿真器。

您可以依次执行示例笔记本电脑的所有行,并在模拟器中观察执行结果。

完整示例:
https://github.com/spiiin/fceux_luaserver/blob/master/FceuxPythonServer.py.ipynb

它包含简单的功能,例如读取内存:



更复杂的回调示例:



以及用于特定游戏的脚本,该脚本可让您从超级马里奥兄弟转移敌人 用鼠标:



笔记本电脑运行视频:


局限性和应用


该脚本没有针对傻瓜的保护措施,并且没有针对执行速度进行优化-最好使用二进制RPC协议而不是文本文本并将消息分组在一起,但是我的实现不需要编译。 该脚本可以将执行上下文从Lua切换到Python,并在我的笔记本电脑上每秒切换500-1000次。 除了视频处理器的逐像素调试或逐行调试的特定情况外,这几乎适用于几乎所有应用程序,但是Fceux仍然不允许Lua进行此类操作,因此没有关系。

可能的应用思路:

  • 作为对其他仿真器和语言实施此类控制的示例
  • 游戏研究
  • 添加作弊或功能来组织TAS段落
  • 在游戏中插入或提取数据和代码
  • 增强仿真器的功能-编写调试器,用于记录和查看演练的脚本,脚本库,游戏编辑器
  • 网络游戏,使用移动设备的游戏控制,远程服务,游戏板或其他控制设备,云服务中的保存和补丁
  • 交叉仿真器功能
  • 使用Python或其他语言库进行数据分析和游戏控制(创建机器人)

技术栈


我用过:

Fceux-www.fceux.com/web/home.html
这是经典的NES模拟器,大多数人都使用它。 它已经很长时间没有更新,并且不是最好的功能,但是它仍然是许多romhacker的默认模拟器。 另外,我之所以选择它,是因为Lua套接字支持已集成到其中,并且无需自己连接。

Json.lua-github.com/spiiin/json.lua
这是纯Lua中的JSON实现。 我选择它是因为我想举一个不需要代码编译的示例。 但是我仍然不得不分叉该库,因为Fceux中内置的某些库使库函数tostring过载并破坏了序列化(我拒绝原始库作者的池请求 )。

Python 3-www.python.org
Fceux Lua服务器打开tcp套接字并侦听从其接收的命令。 将命令发送到仿真器的服务器可以用任何语言实现。 我之所以选择Python是因为其“包含电池”的理念-大多数模块都包含在标准库中(也可以使用套接字和JSON)。 Python还知道用于神经网络的库,我想尝试使用它们在NES游戏中创建机器人。

Jupyter笔记本 -jupyter.org
Jupyter Notebook是一个非常酷的环境,用于交互式执行Python代码。 使用它,您可以在浏览器内部的电子表格编辑器中编写和执行命令。 这对于创建合适的示例也很有帮助。

Dexpot-www.dexpot.de
我使用此虚拟桌面管理器将仿真器窗口停靠在其他窗口之上。 当全屏部署服务器以立即跟踪仿真器窗口中的更改时,这非常方便。 本机Windows工具不允许您将窗口停靠在其他窗口之上。

参考文献


实际上, 是项目存储库

Nintaco-具有远程管理的Java NES模拟器
Xkeeper0 emu-lua集合 -各种Lua脚本的集合
Mesen是C#中的现代NES模拟器,具有强大的Lua脚本功能。 到目前为止,还没有套接字支持和远程控制。
CadEditor是我针对NES和其他平台的通用级编辑器的项目,同时也是研究游戏的强大工具。 我使用帖子中描述的脚本和服务器来浏览游戏并将其添加到编辑器中。

对于反馈,测试和尝试使用脚本,我将不胜感激。

Source: https://habr.com/ru/post/zh-CN435428/


All Articles