Перейти к содержанию

Ломаем RPG Maker XP через RGSSEval


Рекомендуемые сообщения

Всем привет.

 

Спасибо Vasabist, который поднял тему RPG Maker. Он попросил сделать возможность ускорения персонажа при нажатии клавиши Shift.

Мы как-то давно с другом создавали игры на RPG Maker XP и я даже писал какие-то скрипты, расширяющие возможности персонажа.

Тогда уже был VX, но он нам не нравился из-за мелких моделей персонажей. В общем увидел тему, нахлынуло :)

 

Сами игры на RPG Maker XP - по сути большой набор Ruby-скриптов, которые выполняются движком RPG Maker с помощью Ruby-интерпретатора.

 

Сначала я предложил Vasabist использовать фильтры, так как повторяется ситуация с lua-играми - один код работает со множеством адресов.

Но потом вспомнил, что  RPG Maker есть такой класс объектов как Events, который так же позволяет выполнить произвольный код при срабатывании определенных условий. Решил по-исследовать - думал выйти на функцию eval, которой скорее всего и выполняется пользовательский код.

Но всё оказалось даже проще, движок RPG Maker XP предоставляет функцию RGSSEval, которая выполняет произвольный код переданный в аргументах с доступом к глобальным переменным игры. 

 

Собственно на RGSSEval выйти оказалось очень просто. Я скачал RPG Maker XP, создал на карте Event и попросил при взаимодействии с ним вывести строку "Hello, 1234444567":

Spoiler

R1.thumb.png.4b3660e559c2d906835ffc3bd8b

 

После запустил игру и попробовал найти данную строку. И она нашлась! Ставим бряк на access и активируем Event. И в памяти мы видим следующее:

Spoiler

R2.thumb.png.aad079a98da7e88a536910116ee

 

Да, ребят, это оказался memcpy. Но, это не главное. Главное что мы видим смещения адресов как RGSS104E.regex_error_code_to_str+52018, а это значит в движке RGSS104E присутствуют публичные функции, которые он любезно предоставляет нам. И открыв Memory View - View - Enumerate DLL's and Symbols мы видим RGSSEval:

Spoiler

R3.thumb.png.630f4ad2356bcd9f03dc5879a1d

 

Осталось понять как её использовать. В целом на это довольно просто выйти, жмем два раза на функцию данном окне или просто прыгаем на неё и выполняем Memory View - Tools - Dissect Code. В появившемся окне жмем старт и видим всех, кто использует RGSSEval:

Spoiler

R4.thumb.png.7df87336914322b474ea8d7e1fa

 

Двойным нажатием на один из (Call) под RGSSEval прыгаем на место использования и видим, что у данной функции один аргумент (один push с выполняемой строкой перед вызовом) и она не двигает за собой стек (add esp,8 - восемь из-за того, чтобы не двигать стек два раза):

Spoiler

R5.thumb.png.9c9f4866fd1fa367b6f6b6b76c9

 

Собственно, можем копировать эти call-ы в наш AutoAssembler и пробовать вызывать. Для тестирования я набросал следующий скрипт:

Spoiler

[ENABLE]
// выделяем память на вызов функции
globalalloc(run_script, 64)
// выделяем память для текстовой команды на руби
globalalloc(text, 1024)

// Собственно просто просим вывести Hello, World
text:
db 'print "Hello, world!"'

// наш вызов функции
run_script:
// передаем нашу команду 
push text
// вызываем Eval
call RGSSEval
// двигаем стек за собой
pop eax
ret

createthread(run_script)

[DISABLE]
dealloc(run_script)
unregistersymbol(run_script)

dealloc(text)
unregistersymbol(text)

 

 

Запускаем и видим следующее:

Spoiler

R6.thumb.png.fdf410a427ade034d317427fcc8

 

Ура, функция работает как мы и ожидали. Проверить доступные глобальные переменные мы можем через print global_variables, либо обратившись к какой-нибудь из известных нам (или пока только мне..) переменных напрямую, например print $game_player. Можем творить!

 

Собственно весь код игры на Ruby можно увидеть в меню Tools - Script Editor в RPG Maker. И скорее всего код в вашем текущем проекте будет совпадать в большом количестве игр на данном движке. Поэтому можем попытаться написать обертки для стандартных функций для выполнения наших хитрых задач:

Spoiler

R7.thumb.png.c9d928996d033c391097607f9e2

 

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

 

Собственно для того, чтобы ускорить нашего персонажа мы должны воздействовать на атрибут move_speed класса Game_Player. Он является приватным, поэтому для взаимодействия с ним лучше сделать его видимым для всех. Опять же нам повезло и у Game_Player есть метод update, в котором мы сможем проверять нажата ли клавиша Shift, но если бы его не было, пришлось бы менять move_speed из вне.

 

Приступим. Проще всего будет исправить Game_Player создав класс Game_Player унаследовав его от оригинала:

class Game_Player < Game_Player

Для изменения move_speed нам понадобится сохранять его оригинальное значение и назначать скорость бега, выделим для этого две переменные - walk_speed и run_speed. Ну и заодно сделаем move_speed публичным:

  attr_accessor :move_speed
  attr_accessor :walk_speed
  attr_accessor :run_speed

Добавляем функции переключения скоростей и простую проверку на то, бежит ли персонаж:

  def start_run
    @move_speed = @run_speed
  end

  def start_walk
    @move_speed = @walk_speed
  end

  def running?
    return (@move_speed == @run_speed)
  end

Осталось установить начальные значения переменным и добавить проверку на нажатый SHIFT. Для этого обернем функции initialize и update класса Game_Player с помощью функции alias:

  alias orig_initialize initialize

  def initialize
    orig_initialize

    @walk_speed = @move_speed
    @run_speed = 5
  end

Видите? Мы попросили обозначить родительский initialize как orig_initialize и вызывали его в нашей функции initialize.

Скорость бега я установил на 5 (оригинальная скорость 4). Если увеличивать больше - персонаж просто летает и им неудобно управлять.

 

Тоже самое сделаем с update. На нажатый SHIFT проверить очень просто - движок предоставляет объект Input, который знает нажата требуемая клавиша или нет (её я нашел в оригинальных скриптах игры):

  def update
    if Input.press?(Input::SHIFT)
      if !running?
        start_run
      end
    else
      if running?
        start_walk
      end
    end

    orig_update
  end

Собственно и всё - скрипт готов. Если вы добавите его в оригинальные скрипты проекта, при нажатии клавиши SHIFT персонаж будет идти быстрее.

 

Осталось просто заменить наш print "Hello, World" на получившийся скрипт.

Выглядеть это будет следующим образом - каждую строку обрамляем в db 'xxxxx', где xxxx - строчка с кодом. В конце добавляем #13 #10 (каждое число через пробел) - это обычный Enter в конце строки (\n\r - если так привычнее). Я использовал для этого Sublime Text и его мультикурсоры, но можно воспользоваться простой заменой. Ну и не забываем 0 в конце, как конец текста. В итоге мы получим:

Spoiler

db 'class Game_Player < Game_Player' #13 #10
db '  alias orig_initialize initialize' #13 #10
db '  alias orig_update update' #13 #10
db #13 #10
db '  attr_accessor :move_speed' #13 #10
db '  attr_accessor :walk_speed' #13 #10
db '  attr_accessor :run_speed' #13 #10
db #13 #10
db '  def initialize' #13 #10
db '    orig_initialize' #13 #10
db #13 #10
db '    @walk_speed = @move_speed' #13 #10
db '    @run_speed = 5' #13 #10
db '  end' #13 #10
db #13 #10
db '  def running?' #13 #10
db '    return (@move_speed == @run_speed)' #13 #10
db '  end' #13 #10
db #13 #10
db '  def start_run' #13 #10
db '    @move_speed = @run_speed' #13 #10
db '  end' #13 #10
db #13 #10
db '  def start_walk' #13 #10
db '    @move_speed = @walk_speed' #13 #10
db '  end' #13 #10
db #13 #10
db '  def update' #13 #10
db '    if Input.press?(Input::SHIFT)' #13 #10
db '      if !running?' #13 #10
db '        start_run' #13 #10
db '      end' #13 #10
db '    else' #13 #10
db '      if running?' #13 #10
db '        start_walk' #13 #10
db '      end' #13 #10
db '    end' #13 #10
db #13 #10
db '    orig_update' #13 #10
db '  end' #13 #10
db 'end' #13 #10
db 0

 

 

Теперь просто заменяем db 'print "Hello, world!"' на получившийся код и можем запускать в главном меню игры :)

 

Итоговый скрипт:

Spoiler

[ENABLE]
// выделяем память на вызов функции
globalalloc(run_script, 64)
// выделяем память для текстовой команды на руби
globalalloc(text, 1024)

// Дописываем исходный класс
text:
db 'class Game_Player < Game_Player' #13 #10
db '  alias orig_initialize initialize' #13 #10
db '  alias orig_update update' #13 #10
db #13 #10
db '  attr_accessor :move_speed' #13 #10
db '  attr_accessor :walk_speed' #13 #10
db '  attr_accessor :run_speed' #13 #10
db #13 #10
db '  def initialize' #13 #10
db '    orig_initialize' #13 #10
db #13 #10
db '    @walk_speed = @move_speed' #13 #10
db '    @run_speed = 5' #13 #10
db '  end' #13 #10
db #13 #10
db '  def running?' #13 #10
db '    return (@move_speed == @run_speed)' #13 #10
db '  end' #13 #10
db #13 #10
db '  def start_run' #13 #10
db '    @move_speed = @run_speed' #13 #10
db '  end' #13 #10
db #13 #10
db '  def start_walk' #13 #10
db '    @move_speed = @walk_speed' #13 #10
db '  end' #13 #10
db #13 #10
db '  def update' #13 #10
db '    if Input.press?(Input::SHIFT)' #13 #10
db '      if !running?' #13 #10
db '        start_run' #13 #10
db '      end' #13 #10
db '    else' #13 #10
db '      if running?' #13 #10
db '        start_walk' #13 #10
db '      end' #13 #10
db '    end' #13 #10
db #13 #10
db '    orig_update' #13 #10
db '  end' #13 #10
db 'end' #13 #10
db 0

// наш вызов функции
run_script:
// передаем нашу команду
push text
// вызываем Eval
call RGSSEval
// двигаем стек за собой
pop eax
ret

createthread(run_script)

[DISABLE]
dealloc(run_script)
unregistersymbol(run_script)

dealloc(text)
unregistersymbol(text)

 

 

Итоговый скрипт на Ruby:

Spoiler

class Game_Player < Game_Player
  alias orig_initialize initialize
  alias orig_update update

  attr_accessor :move_speed
  attr_accessor :walk_speed
  attr_accessor :run_speed

  def initialize
    orig_initialize

    @walk_speed = @move_speed
    @run_speed = 5
  end

  def running?
    return (@move_speed == @run_speed)
  end

  def start_run
    @move_speed = @run_speed
  end

  def start_walk
    @move_speed = @walk_speed
  end

  def update
    if Input.press?(Input::SHIFT)
      if !running?
        start_run
      end
    else
      if running?
        start_walk
      end
    end

    orig_update
  end
end

 

 

Видео работы скрипта:

Spoiler

Для видео дополнительно добавил звук ускорения - $game_system.se_play($data_system.escape_se)

 

 

 

 

Итого (или  рубрика "О проблемах"):

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

 

Вот и всё, народ )

    • Плюс 5
    Ссылка на комментарий
    Поделиться на другие сайты

    6 часов назад, srg91 сказал:

    Всем привет.

    srg91, круто!!! Очень круто!!!

    Только:

    1. я бы перенёс твои статьи в раздел "Статьи для продвинутых". - тут уже нужен более продвинутый уровень в GH и программировании ИМХО.

    2. может всё-таки уроки попробуешь в видео формате (с голосом конечно) - лучше 1 раз увидеть, чем много раз прочитать.

    Ссылка на комментарий
    Поделиться на другие сайты

    39 minutes ago, Garik66 said:

    srg91, круто!!! Очень круто!!!

    Только:

    1. я бы перенёс твои статьи в раздел "Статьи для продвинутых". - тут уже нужен более продвинутый уровень в GH и программировании ИМХО.

    2. может всё-таки уроки попробуешь в видео формате (с голосом конечно) - лучше 1 раз увидеть, чем много раз прочитать.

     

    Спасибо :) 

     

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

    2. В целом я структурировал текст, возможно попробую на досуге еще раз записать видео. Если получится - дозалью в "Видео" и добавлю ссылочку в начало статьи.

    Ссылка на комментарий
    Поделиться на другие сайты

    Серег, круто это) пасиб. так что у тебя со своей игрой то? дальше писать будешь?)

    Изменено пользователем Vasabist
    Ссылка на комментарий
    Поделиться на другие сайты

    [offtop]Это было далекие года назад, вряд-ли :) Плюс в основном писал не я, я больше по скриптам - зеркало там забабахать, сохранение инвентаря, etc. У меня упорства никогда не хватает - закончить хоть одну игру )[/offtop]

    Ссылка на комментарий
    Поделиться на другие сайты

    ×
    ×
    • Создать...

    Важная информация

    Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.