Как мы распаковку игры автоматизировали

в 9:47, , рубрики: c++, win api, разработка под windows, Спортивное программирование

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

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

Как мы распаковку игры автоматизировали - 1

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

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

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

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

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

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

Как мы распаковку игры автоматизировали - 2

Как мы распаковку игры автоматизировали - 3

Как мы распаковку игры автоматизировали - 4

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

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

Как мы распаковку игры автоматизировали - 5

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

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

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

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

Как мы распаковку игры автоматизировали - 6

Получать 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++ и смотрим что у нас внутри:

Как мы распаковку игры автоматизировали - 7

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

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

Автор: nanshakov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js