Мое хобби, помимо программирования — разработка модификаций под игру S.T.A.L.K.E.R. Работаем мы в команде, где, как и принято, каждый отвечает за что-то свое. Я, помимо того, что вхожу в круг разработчиков, еще и осуществляю разработку ПО для команды. Под катом читайте, как мы автоматизировали распаковку игровых архивов, с какими проблемами столкнулись и как их решили.

Ресурсы игры запакованы в архивы. Оценить сколько у нас архивов в текущем билде вы можете ниже:



У разработчиков все эти архивы есть в распакованном виде, а для тесторов мы выпускаем куммулятивные обновления, распространяющиеся единым архивом.

Помимо этого, существует еще и разные наборы изменений из 4-х нижних архивов. Иногда возникает необходимость распаковать их все. У нас есть разные инструменты для упаковки и распаковки, я знаю, как минимум, 2 набора – консольная и GUI версия. У каждой из них свои недостатки:

Консольная:
1. На распаковке архивов более ~ 1.6 ГБ падает.

GUI:
1. Выбор начальной папки, архива, конечной директории – производится руками.
2. возможна только распаковка 1 архива за раз.

Поскольку у нас есть архивы и почти по 2ГБ, то приходится использовать GUI. Когда мне надоело по сто раз делать одно и тоже, я решил его автоматизировать.

Распаковщик имеет вот такой интерфейс:







Это все диалоговые окна. Меня навело на мысль поле, в котором отображается имя файла. Если его можно вписать туда руками или выбрать, то это же можно повторить и программно.

Мы вооружимся C++ WinAPI и SPY++ и пока будем работать над первым окном. Запустим распаковщик и SPY++, найдем там его процесс:



И да, для того, чтобы было удобнее искать нужные поля на экране, советую их заполнить информацией, я, например, выбрал архив. Хорошо, поля мы тут видим. Можно приступать писать код…

Еще при первом обдумывании идеи у меня возникла мысль сделать удобный конфигурационный файл. Структура его была придумала сразу и так и не менялась:

Распаковщик.exe – пусть до GUI распаковщика
C:\S.T.A.L.K.E.R\ – пусть до игры
D:\Stalker SHоC\1\ – пусть до папки, в которую надо распаковать
YES – получить список архивов рекурсивно (NO) или из списка ниже
gamedata.db1 – список с именами архивов
/gamedata.db2 – закомментированная строчка

Я не буду подробно останавливаться на том, как считывается конфиг. Скажу лишь то, что у нас есть структура с аналогичными полями. Для того, чтобы нажимать на кнопочки в другом окне нам нужно получить его handle. Далее мы должны получить handlы нужных нам элементов управления. Причем, получать их именно в той последовательности, в которой они связаны (то, как они связаны видно по раскрывающимся спискам у элементов в SPY++. Посмотрим:



Получать handlы мы это будем следующим кодом:

HWND hwnd = FindWindow(NULL, "Select file to unpack...");
HWND hbnd = FindWindowEx(hwnd, NULL, "Button", "&Открыть");
HWND hсnd = FindWindowEx(hwnd, NULL, "ComboBoxEx32", "");
hсnd = FindWindowEx(hсnd, NULL, "ComboBox", NULL);
hсnd = FindWindowEx(hсnd, NULL, "Edit", NULL);
#ifdef _DEBUG 
BOOST_LOG_TRIVIAL(info) << "FindWindow(NULL, 'Select file to unpack...') " << hwnd;
BOOST_LOG_TRIVIAL(info) << "FindWindow(NULL, 'Button', '&Открыть') " << hbnd;
BOOST_LOG_TRIVIAL(info) << "FindWindow(NULL, 'Edit', '') " << hсnd;
#endif

Функция FindWindow(Ex) — возвращает handle по имени объекта. Вторым параметром может принимать значение класса объекта, а первым можно передать тот объект, в котором искать (например, мы передаем handle окна для поиска кнопки).

После того, как мы получили данные, нам нужно послать сообщение элементу управления. Делать мы будем это так:

if ((hwnd != NULL && hbnd != NULL ) && hсnd != NULL)
 {
  //устанавливаем текст
  SendMessage(hсnd, WM_SETTEXT, 0, (LPARAM)(LPCTSTR(path_to_db.c_str())));
  //кликаем
  SendMessage(hbnd, WM_LBUTTONDOWN, 0, 0);
  SendMessage(hbnd, WM_LBUTTONUP, 0, 0);
  #ifdef _DEBUG
  BOOST_LOG_TRIVIAL(info) << "Sended";
  #endif
}

Здесь я остановлюсь подробнее, так как возникли трудности при заполнении ComboBoxа. Изначально код был немного другим, и я получал только:

HWND hсnd = FindWindowEx(hwnd, NULL, "ComboBoxEx32", "");

И пытался заполнить его используя:

SendMessage(hсnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR(path_to_db.c_str())));

Но ничего не получалось. Почему – я не знаю до сих пор. Еще одна проблема была в том, что меня смутил принцип работы этого окна. На скриншоте выше видно, что когда мы выбираем архив то в ComboBox попадает только его название. Путь нигде не фигурирует. Надеясь на чудо, я передал туда полный путь до архива и все заработало. Чудеса есть? Думаю, нажатие на кнопку Ок очевидно, и мы его рассматривать не будем.

Теперь пришло время работы со вторым окном выбора папок. Сразу приведу код и потом прокомментирую.

//работа со 2м окном
Sleep(time);
hwnd = FindWindow(NULL, "Обзор папок");
hbnd = FindWindowEx(hwnd, NULL, "Button", "ОК");
hсnd = FindWindowEx(hwnd, NULL, "Edit", NULL);
if ((hwnd != NULL && hbnd != NULL) && hсnd != NULL)
{
//устанавливаем текст
SendMessage(hсnd, WM_SETTEXT, 0, (LPARAM)(LPCTSTR(config.path_to_output.c_str())));
//кликаем
SendMessage(hbnd, WM_LBUTTONDOWN, 0, 0);
SendMessage(hbnd, WM_LBUTTONUP, 0, 0);
#ifdef _DEBUG
BOOST_LOG_TRIVIAL(info) << "Sended";
#endif
}

С этим окном тоже появились проблемы. На нем нет никаких видимых элементов управления, кроме кнопок. Открываем SPY++ и смотрим что у нас внутри:



Я стал выбирать разные папки, и имя директории заносилось в элемент управления Edit, опять-таки, только имя (там что, обработчик клика мыши их в строку складывает?). И я совсем отчаялся, потому что не мог представить, как же мне свой путь туда передать. Первая мысль была запустить все это под отладчиком и найти тот адрес памяти, где лежит конечный путь, инжектить в процесс свою dll и менять значение в памяти (а DEP даст это сделать?).

Эта идея провалилась, потому что я не умею работать с отладчиком и никогда такое не делал, а учиться надо начинать с более простого. Снова надеясь на чудо, я передал в Edit полный путь — и все заработало! Радости не было придела. Далее было рутинное дописывание кода, с которым вы можете ознакомиться тут. Мы получили удобный и универсальный инструмент для распаковки. Я думаю, что мое решение проблемы не единственное и буду рад, если в комментариях расскажут что-то полезное. Спасибо за внимание.

Комментарии (16)


  1. lacki
    21.12.2015 14:19
    +1

    inqsoft sign 0f misery позволял программировать это кликами мышкой. Не знаю работает ли он на современных версиях Windows, но раньше спасал кучу времени на подобных задачах.


  1. Prototik
    21.12.2015 16:09
    +14

    Как-то кривенько. Не лучше ли было найти/реализовать алгоритм распаковки самостоятельно и к нему уже написать обвес в виде GUI/CLI?


    1. AllexIn
      21.12.2015 17:35
      +1

      Нет, конечно.
      Надстройка над GUI делается за полчаса.
      А искать(и уж тем-более реверсить) и реализовывать алгоритм распаковки/запаковки — значительно более долгая задача.
      Ну и ключевой вопрос: А зачем?
      Ради чего городить забор, если можно быстро и с минимумом затрат реализовать простое и эффективное решение?


    1. nanshakov
      21.12.2015 20:01
      +1

      Это есть в планах. Просто команде нужен был инструмент и быстро. А что именно кривенько?


  1. AtomKrieg
    21.12.2015 16:58
    +2

    Попробуйте autohotkey


  1. Mingun
    21.12.2015 19:37

    А файлик Пароль.txt точно должен быть в репозитории? :)


    1. nanshakov
      21.12.2015 19:58

      Да, у меня возникли проблемы с выгрозкой кода в репозиторий, это пароль от Debug — сборка.7z. Как видно из хаоса в репозитории, я только начинаю разбираться с гитом.


  1. gouranga
    21.12.2015 21:15
    +11

    Так у этой GUI-утилиты есть параметры командной строки…
    Распаковщик.exe <gamedata.db?> [output-directory]


    1. nanshakov
      21.12.2015 22:29
      +1

      Чувствую себя дураком (. А как вы это нашли?


      1. gouranga
        21.12.2015 23:23

        Вообще гуглом нашел архив. А внутри был Readme.txt…

        А если бы не нашел, то рассматривал вариант, который предложил Prototik. Мы же это чисто теоретически да, в научных интересах? :o)
        1) распаковал бы exe утилиты или игры, если необходимо. Утилита упакована обычным upx — тут даже навыки никакие не нужны, существует миллион распаковщиков (например, встроенный в PE Explorer).
        2) посмотрел бы в IDA (или в другом инструменте), что там и куда вызывается.

        Спасибо Ida Pro и Hex-Rays

        Чтобы поправить псевдокод Hex-Rays до вида на картинке потребовалось пара минут. И стало видно, что readme не врет.

        А дальше можно разобрать формат архивов (unpack_procedure на картинке).


        1. AllexIn
          22.12.2015 10:05

          IDA стоит денег и больших. Далеко не у каждого разработчика она есть. Тем более, что далеко не каждому разработчику она нужна.


        1. nanshakov
          22.12.2015 18:33
          +1

          Спасибо, я попробую повторить шаги.


  1. ComodoHacker
    22.12.2015 10:19
    +1

    Полученный опыт это здорово, но с практической точки зрения на AutoIt это делается в три строчки (если бы утилита не принимала параметры, как уже заметили). Посмотрите, может еще пригодится.


    1. nanshakov
      22.12.2015 18:33

      Посмотрю, спасибо.


  1. goghAta
    22.12.2015 13:40

    По поводу того, почему не работало сообщение CB_ADDSTRING, а сообщение WM_SETTEXT срабатывало. Я думаю это внутренний механизм Винды здесь выходит на сцену. Когда сообщение шлется эдит контролу, и оно содержит в себе указатель на память из другого процесса (программа автоматизатор щлет строку распоковщику), то винда сериализует буфер строки и в таргет процессе уже использует адрес буфера, валидный в контексте таргет процесса. На сколько я помню, это было сделано для обратной совместимости со всякими Win95. В случае с CB_ADDSTRING и ComboBoxEx32, возможно, этот механизм не работает.


    1. nanshakov
      22.12.2015 18:39

      Как не явно — то. Спасибо, не знал о таком.