Главная » Хабрахабр » Удалённое управление эмулятором Fceux с помощью Python

Удалённое управление эмулятором Fceux с помощью Python

В статье я опишу, как сделать эмулятор NES управляемым удалённо, и сервер для удалённой отправки команд на него.

Зачем это нужно?

Некоторые эмуляторы различных игровых консолей, в том числе и Fceux, позволяют писать и запускать пользовательские скрипты на Lua. Но Lua – плохой язык для написания серьёзных программ. Это скорее язык для вызова функций, написанных на Си. Авторы эмуляторов используют его только из-за легковесности и простоты встраивания. Точная эмуляция требует много ресурсов процессора, и ранее скорость эмуляции была одной из главных целей авторов, а о возможности скриптования действий если и вспоминали, то далеко не в первую очередь.

Сейчас мощности среднего процессора с головой хватает для эмуляции NES, почему бы тогда не использовать в эмуляторах мощные скриптовые языки вроде Python или JavaScript?

Я обнаружил только малоизвестный проект Nintaco, который также основан на ядре Fceux, зачем-то переписанном на Java. К сожалению, ни в одном из популярных эмуляторов NES нет возможности использовать эти или другие языки. Тогда я решил добавить возможность написания скриптов на Python для управления эмулятором сам.

Я делал его для себя, но так как вопрос о том, как управлять эмулятором с помощью скриптов, встречается достаточно часто, то я выложил исходники на гитхаб.
Мой результат – это Proof-of-Concept возможности управления эмулятором, он не претендует на скорость или надёжность, но он работает.

Как это устроено

На стороне эмулятора

Эмулятор 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 для приостановки и продолжения выполнения кода.

Каким образом организовать продолжения выполнения Lua-кода и вновь запустить её командой, полученной из сокета? И ещё важная одна вещь про Lua-скриптинг в Fceux – эмуляция может быть временно остановлена из скрипта. Это было бы невозможно, но существует плохо документированная возможность вызвать Lua-код даже при остановленной эмуляции (спасибо feos за то, что навёл на неё):

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")

С помощью этого класса Lua-методы эмулятора Fceux могут быть обёрнуты в 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-колбека управление возвращается в 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-прохождений
  • Вставка или извлечение данных и кода в игры
  • Расширение возможностей эмуляторов — написание отладчиков, скриптов записи и просмотра прохождений, скриптовых библиотек, редакторов игр
  • Сетевая игра, контроль игры с помощью мобильных устройств, удалённых сервисов, джойпадов или других устройств управления, сохранение и патчи в облачных сервисах
  • Кросс-эмуляторные фичи
  • Использование библиотек языков Python или других для анализа данных и управления игрой (создание ботов)

Стек технологий

Я использовал:

Он не обновлялся уже долгое время, и не лучший по возможностям, но он остаётся эмулятором по умолчанию для множества ромхакеров. Fceux — www.fceux.com/web/home.html
Это классический эмулятор NES, и большинство людей используют его. Также, я выбрал его из-за того, что в него интегрирована поддержка Lua-сокетов, и нет необходимости подключать её самому.

Я выбрал её, потому что хотел сделать пример, который не требует компиляции кода. Json.lua — github.com/spiiin/json.lua
Это реализация JSON на чистом Lua. Но мне всё равно пришлось сделать форк библиотеки, потому что какая-то из встроенных во Fceux библиотек перегружала библиотечную функцию tostring и ломала сериализацию (мой отклонённый пул-реквест автору оригинальной библиотеки).

Сервер, который посылает команды эмулятору, может быть реализован на любом языке. Python 3 — www.python.org
Fceux Lua сервер открыват tcp-сокет и слушает команды, полученные от него. Также Python известен библиотека работы с нейронными сетями, и мне хочется попробовать использовать их для создания ботов в NES-играх. Я выбрал Python за его философию «Battery included» – большинство модулей включены в стандартную библиотеку (работа с сокетами и JSON в том числе).

С помощью неё вы можете писать и выполнять команды в табличном редакторе внутри браузера. Jupyter Notebook — jupyter.org
Jupyter Notebook – очень крутая среда для интерактивного выполнения Python-кода. Он также хорош для создания презентабельных примеров.

Это очень удобно при разворачивании сервера на полный экран для мгновенного отслеживание изменений в окне эмулятора. Dexpot — www.dexpot.de
Я использовал этот менеджер виртуальных рабочих столов, для того, чтобы закреплять окно эмулятора поверх других. Штатные средства Windows не позволяют организовать закрепление окна поверх других.

Ссылки

Собственно, репозиторий проекта.

Пока без поддержки сокетов и удалённого управления.
CadEditor — мой проект универсального редактора уровней для NES и других платформ, а также мощные инструменты для исследования игр. Nintaco — эмулятор NES на Java с возможностью удалённого управления
Xkeeper0 emu-lua collection — коллекция различных Lua-скриптов
Mesen — современный эмулятор NES на C# с мощными возможностями написания скриптов на Lua. Я использую скрипт и сервер, описанные в посте, для того, чтобы исследовать игры и добавлять их в редактор.

Буду признателен за отзывы, тестирование и попытки использования скрипта.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

WebAssembly в продакшне и «минное поле» Smart TV: интервью с Андреем Нагих

Разработка приложений под Smart TV — тоже «нетипичный JavaScript», когда все слышали о чём-то, но немногие лично пробовали. Интерес к WebAssembly велик, но пока что нечасто встретишь людей, использующих эту технологию в рабочем проекте. TV, а в последние месяцы так ...

[Перевод] Ethereum планирует стать на 99% экономичней

Криптовалюта скоро сядет на энергетическую диету, чтобы конкурировать с более эффективными блокчейнами На фоне ажиотажа вокруг Биткоина его «младший брат» Ethereum отошел в тень. Но проект с рыночной капитализацией около 10 млрд долларов вряд ли можно считать незаметным. И объемы ...