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