Spiiin's blog

Скриптинг в эмуляторах.

Давно в голове крутилась такая идея - взять эмулятор одной из старых консолей с поддержкой скриптов, добавить в его систему парсинга команд специальный опкод, по которому будет срабатывать передача управления в скрипт. Дальше вместо стандартного процесса хака делать следующее - дизассемблированную функцию переписать на скриптовом языке и стереть её из ROM, заменив тело на опкод вызова скриптовой функции (для этого хватит пары байт), оставшееся место использовать для своих нужд. Из скрипта есть доступ к любым функциям эмулятора (а если и нет, можно прокинуть нехватающие).

Так можно переписать все основные системы игры - управление, интеллект врагов, форматы описания уровней, тексты etc. Всё необходимое для модификации. На выходе получится практически чистый ROM с частью технических неинтересных для модификации функций в нём, и движок игры в читабельном виде на языке высокого уровня.

Сложность переписывания кода на скриптовом языке намного меньше, чем переписывания алгоритма на ассемблере, но главные плюсы в том, что снимаются ограничения на размеры данных и кода, которых всегда не хватает, а также в том, что можно удобно тестировать переписанный код, модифицируя его без перезапуска игры и работать над скриптом совместно.
Так еще никто не делал :)

Решил попробовать реализовать такое для FCEUltra, так как у него открытые исходники и есть поддержка lua-скриптов, используемых для читерства и тассинга. Ее можно расширить и для программирования.

В конце функции эмуляции выполнения команд процессора X6502_Run можно дописать что-то вроде:

...
#ifdef _S9XLUA_H
if (b1 == 0x3A) //байт 0x3A назначен опкодом выполнения lua-функции
{
uint8 x; //чтение номера вызываемой функции
x=RdMem(_PC);
_PC++;
CallLuaInjectedFuncion(x);//вызов lua-функции
}
#endif
}
...

void CallLuaInjectedFuncion(int function_index)
{
if (L)
{
lua_getfield(L, LUA_GLOBALSINDEX, "inject_handler"); //положить на стек таблицу
if (lua_isfunction(L,-1))
{
lua_pushinteger(L, function_index); //взять из таблицы функцию номером function_index
int ret = lua_pcall(L,1,0,0); //вызвать функцию
if (ret)
HandleCallbackError(L); //стандартная для fceu обработка ошибок
}
}
}

Еще очень удобно добавить в lua возможность выполнить нативный код, чтобы свободно смешивать уже написанную игровую логику на ассемблере со скриптовым кодом:

void X6502_CallFunc(uint16 funcAddr)
{
uint16 oldPC;
int callLevel;
uint8 b1;

callLevel = 1;
oldPC = _PC; //запомнить счетчик команд
_PC = funcAddr; //и переставить его на адрес функции
while (true)
{
b1=RdMem(_PC);
if ((b1 == 0x60) && (--callLevel == 0)) //если следующая команда RET и исполнение наверху стека вызовов, то прекратить выполнение
break;
else if (b1 == 0x20) //если следующая команда JSR, то увеличить глубину вызовов
callLevel++;
X6502_Run(1); //исполнить следующую команду
}
_PC = oldPC; //вернуть состояние счетчика команд
}

Дальше для теста переписал небольшую часть логики прыжков Черного Плаща под новую версию эмулятора. Получился такой скрипт:
https://gist.github.com/4145710
Ничего интересного он не делает, только демонстрирует потенциал для ромхакинга.

Целиком эмулятор, ром и скрипт выложил в теме тут http://www.emu-land.net/forum/index.php/topic,64174.0.html