Pull to refresh

Как я получил ключ к Diablo III Beta

Reading time 6 min
Views 57K
В YouTube роликах ThisIsHorosho с недавних пор стали появляться ключи к Diablo III Beta. В 7-ми минутном ролике на секунду показывается ключ, кто его первый активирует, то и выигрывает. Вот так на стоп кадре выглядит ключ:


Вы подумали о том же, о чем и я?


Всего 10 ключей.

Ключ 1
О первом ключе я не знал и, наверное, никто не знал. Кому-то повезло.

Ключ 2
Ко второму видео был готов самый простой бот, который раз в 10 секунд обновляет страницу с добавленным видео и, если появилось новое видео, то модальное окно в виде алерта сообщает об этом. Выяснилось, что через 4 часа youtube обнаруживает бота и просит его ввести recaptcha, а потом еще каждый час просит ее вводить. Очень не удобно, но я не особо хотел тратить время и писать по-нормальному, так как ожидал, что второй ключ появится как-нибудь по-другому, ну например, хотя бы зелеными буквами. Второй ключ я проморгал, но когда я увидел те же серые цифры и буквы, тем же шрифтом, того же размера, на том же белом фоне и даже размером в те же три строчки, я решил сделать нормального бота.

Ключ 3
К третьему видео бот использовал youtube api, квота на кол-во запросов к которому позволяла сканирование раз в 5 секунд на протяжении 12 часов.

Алгоритм был простой:
  1. Каждые 5 секунд делаем GET к http://gdata.youtube.com/feeds/api/users/ThisIsHorosho/uploads?max-results=1&fields=openSearch:totalResults,entry/id и получаем общее количество видео пользователя и ссылку на последнее видео.
  2. Если количество видео изменилось, то из последней ссылки получаем videoId: 3J1CYzzZjNc
  3. Делаем GET к http://www.youtube.com/get_video_info?video_id=3J1CYzzZjNc и получаем url encoded параметры
  4. Берем параметр url_encoded_fmt_stream_map — это url encoded прямые ссылки на видео разных форматов и качества. Ссылка работает только для того IP, с которого пришел запрос. Пошаманив, можно получить красивую ссылку http://o-o.preferred.lhr14s07.v2.lscache8.c.youtube.com/videoplayback?sparams=cp,id,ip,ipbits,itag,ratebypass,source,expire&fexp=906929,907720,904821&itag=18&ip=88.0.0.0&signature=BA6D9C66CA9DF74931C899ABC3816E6FFB3AF2B5.326CFD03BDE8430990DEE9E8DC62046FAC43C62B&sver=3&ratebypass=yes&source=youtube&expire=1332983106&key=yt1&ipbits=8&cp=U0hSR1lTUV9FUENOMl9RTVVCOmNpbEFrX1hXTllx&id=dc9d42633cd98cd7
  5. Бот, скачав ролик, приступает к анализу видео с помощью vlc библиотеки, которая понимает из коробки mp4,flv,webm.
  6. Сканируя кадры через 1 секунду, находим и сохраняем те, на которых предположительно находится серийный номер. Сканируем в 4 потока, каждый свою 1/4 часть видео.
  7. Пользователю, т.е. мне, остается только посмотреть найденные скриншоты и ввести ключ

Итак, выскочил алерт, сообщающий о выходе нового видео, я залогинился к battle.net, через 10 секунд скачалось видео, еще через 10 появился скриншот с серийным номером:

Я его ввел и… он оказался уже использованым. Анализ ошибок показал, что данные через youtube api обновляются с задержкой до 5 минут! Когда бот обнаружил новое видео, уже было слишком поздно. Кому-то из других ботов или пользователей жмущих F5 повезло.

Ключ 4
К четвертому видео, бот был доработан: сейчас он использует порядка 100 прокси серверов (по 1 потоку на каждый прокси сервер), которые сканируют каждые 5 секунд gdata. Тесты показали, что около 20 прокси просто мгновенно сообщают о выходе нового видео, остальные подтягиваются в течении минуты, это было отлично. В отличии от gdata прямые ссылки появляются сразу для любого IP, поэтому тут без прокси бот просто качает видео в 20 потоков (работало даже в 1000 потоков, youtube оказывается позволяет). Скорость закачки возросла. Алгоритм сканирования был доработан: бот вначале в 4 потока сканирует вторую часть ролика (во всех предыдущих видео серийный ключ появлялся в конце ролика), а потом в 4 потока первую часть. Для надежности шаг сканирования был уменьшен до 500 мс, другие параметры так же были немного ослаблены.

Итак, выскочил алерт, сообщающий о выходе нового видео. Не успел я зайти в battle.net как уже скачалось видео (10 секунд) и появился скриншот с серийным номером:


Я порадовался, что учел возможность его появления в две строчки. Трясущимися руками я его кое-как написал и активировал! На все ушло секунд 20. Очень повезло со сканированием, алгоритм практически сразу показал серийный номер при том, что полное сканирование продолжалось 30 секунд. В этом ролике, как оказалось, было два ключа, которые показались последовательно, я ввел второй. Поздравляю того, кто активировал первый!

Осталось еще 5 ключей
Можно доработать алгоритм: запускать сканирование вместе с началом старта скачивания, алгоритм усложнится, но выигрышь будет секунд 10. Можно еще сделать распознование серийного номера и его автоматический ввод в battle.net. Тогда ключик можно будет ввести даже за 5 секунд.

Все писалось на Java, используя HttpComponents (http-протокол) и VLCj (обработка видео)

P.S. Diablo III клевая

UPDATE
Интересно было писать самого бота, поэтому затраченные 20 часов я расцениваю как время потраченное на развлечение, а не как 20 часов, за которые можно было бы заработать гораздо больше денег, чем стоит сам ключ. В свободное время я отдыхаю или изучаю что-то новое, а не работаю, а тут приятное с полезным. Игру еще не прошел.

Алгоритм
Алгоритм определения серийника я специально сразу не стал указывать по двум причинам. Узнав абсолютно точный алгоритм, авторы ThisIsHorosho быстро сделают ключ нераспознаваемым, и я окажу медвежью услугу тем, кто тоже пишет бота. Хотя допускаю, что таких людей нет, но как то же, судя по комментариям, за 3 минуты вводят серийники, неужели жмут F5 во время ожидания…

Ну раз много вопросов по алгоритму… Главное, алгоритм должен быть очень быстрый. По скриншоту с серийным номером сразу видна основная идея.

  1. Берем картинку из кадра и сохраняем ее с размером 640x320, VLCj позволяет сохранять картинки с любым разрешением, даже, если видео имеет другое. Все точки, близкие к цвету текста делаем черными, все остальное белое. В итоге получаем черно белые картинки. Пару таких картинок вставлены в эту статью.
  2. Для каждого скриншота считаем статистику белых и черных точек. Фоном объявляем те, где количество белых точек больше 92%, в тестах хватало и 94%, но это с запасом. На кадрах с фоном ищем серийник.
  3. Из кадра с краев отступаем по 30 пикселей, так как серийник появляется ближе к центру, а с края — никогда. Оставшееся поле разбиваем на квадраты 20x20, в каждом из которых считаем количество черных точек
  4. Квадраты с количеством черных точек от 10% до 60% объявляем квадратами с буквами — это уже с учетом того, что буква может только на половину попасть в квадрат и с некоторым запасом.
  5. Кадры, на которых есть непрерывная последовательность из как минимум 6 квадратов с буквами по горизонтали и в 3 квадрата по вертикали, объявляем кадрами с серийным номером. Сохраняем их в папку.


В итоге, появляются только картинки с текстом похожим на серийный номер:


На практике их не много появляется, так что найти из них нужный не составляет труда.

Работа с VLCj
Работа с VLCj очень простая. Вначале я прочитал документацию www.capricasoftware.co.uk/vlcj/tutorial1.php, потом немного поигрался с классом MediaPlayer, но он как-то глючил, в общем я остановился на вызове прямых функций из библиотеки LibVlc — оно и быстрее и безглючнее.

Вначале создаем библиотеку
LibVlc libvlc = LibVlcFactory.factory().create();

Потом создаем массив из 8 штук AnalyzerThread (типа Runnable) (каждому 1/8 часть времени видео), которые передаем в Executors.newFixedThreadPool(4) в таком порядке: 4, 5, 6, 7, 0, 1, 2, 3. Т.е. вначале будет сканироваться вторая часть видео, а потом первая. В каждом AnalyzerThread такой код:

System.out.println("Run section " + num);
libvlc_media_player_t p_mi = null;
libvlc_media_t media = null;
try
{
    // prepare
    //libvlc_instance_t instance = libvlc.libvlc_new(0, new String[0]);
    libvlc_instance_t instance = libvlc.libvlc_new(2, new String[]{"--vout", "dummy"});
    p_mi = libvlc.libvlc_media_player_new(instance);
    libvlc.libvlc_audio_toggle_mute(p_mi);
    media = libvlc.libvlc_media_new_path(instance, fileName);
    libvlc.libvlc_media_player_set_media(p_mi, media);
    libvlc.libvlc_media_player_play(p_mi);
    Thread.sleep(msPlayerWait);
    libvlc.libvlc_media_player_pause(p_mi);

    // start snapshoting
    int block = blockFrom;
    for (long msTime = msFrom; msTime <= msTo; msTime += msInBlock, block++)
    {
        String path = snapshotPath + File.separator + "snap-" + String.format("%03d.png", block);
        libvlc.libvlc_media_player_set_time(p_mi, msTime);

        int r = libvlc.libvlc_video_take_snapshot(p_mi, 0, path, picWidth, picHeight);
        if (r != 0)
            System.out.println("SNAPSHOT FAILED: block=" + block + ", returnCode=" + r);
        else
            analyzeImage(path);
    }
} finally
{
    if (p_mi != null)
        libvlc.libvlc_media_player_stop(p_mi);
    if (media != null)
        libvlc.libvlc_media_release(media);
    if (p_mi != null)
        libvlc.libvlc_media_player_release(p_mi);
    System.out.println("Close section " + num);
}


Функция analyzeImage как раз определяет, находится ли на скриншоте ключик или нет, если находится, то сохраняет его в специальную папку.
Tags:
Hubs:
+310
Comments 158
Comments Comments 158

Articles