Лаунчер для Electronvolt

Майские праздники не прошли даром: на пару дней я основательно засел за Python и написал собственный лаунчер. Зачем? Он нужен для того, чтобы не привязывать игру к конкретной игровой платформе. В моем случае, я использую GameJolt, но такой подход работает для любого сервиса. Лаунчер служит посредником между игрой и API площадки. Все, что нужно делать игре — это сообщать лаунчеру о событиях наподобие «игрок получил такую-то ачивку». Лаунчер, в свою очередь, передает эти данные игровому сервису. Если надо переехать на другой сервис, достаточно просто обновить лаунчер, а игру патчить не придется — это удобно и экономит массу времени.

Для создания интерфейса я использовал WebView. Можно спорить до хрипоты о недостатках веб-стека, но на сегодняшний день он остается самым простым и универсальным решением для создания GUI. Нативные тулкиты платформозависимы, требуют соблюдения особой архитектуры приложения и чаще всего выглядят скучновато, а браузерный движок позволяет буквально за несколько часов наваять красивые формы с анимациями и спецэффектами — сделал и забыл!

Долгое время я писал веб-интерфейсы на CEF (Chromium Embedded Framework) — «Electron для Python». Он предоставляет portable браузерный движок и в целом свою задачу решает, но имеет ряд проблем. CEF Python уже несколько лет как не развивается, и версия Chromium в нем заметно устарела, новейшие свойства CSS не поддерживаются. Кроме того, в собранном виде CEF-приложение весит около 200 Мб, что для небольшого приложения, мягко говоря, жирновато. Все эти проблемы я решил переходом на PyWebView! Единственное, чем придется пожертвовать — совместимостью с Windows 8, что в 2025 году совсем не критично. Собранный PyInstaller’ом, лаунчер стал весить менее 50 Мб (используя UPX, можно сжать еще сильнее). А код под PyWebView гораздо компактнее и проще, чем под CEF Python.

Основная задача моего лаунчера — служить посредником между игрой и внешними сервисами. Для этого используется механизм IPC: игра отправляет события лаунчеру через TCP-соединение по кастомному протоколу. Например, когда игрок получает достижение, игра отправляет команду типа "eV:award=AchievementName". Лаунчер, получив такую команду, передает данные на сервер.

Важная особенность этого механизма — асинхронность: блокировать игровой цикл, чтобы отправить что-то по сети — не комильфо, задержки бывают в секунду и даже дольше. Выручил taskPool из std.parallelism — все вызовы IPC оборачиваются в task и заносятся в очередь пула задач:

taskPool.put(task!ipcSendSync(message));

Это не блокирует поток и обеспечивает плавный игровой процесс. Кроме того, очередь гарантирует, что вызовы будут обработаны последовательно — не будет состояния гонки за исходящий сокет, если отправить несколько сообщений подряд.

GameJolt имеет простой и удобный API, который легко интегрировать в проект. Однако, чтобы не компрометировать приватный ключ, я решил не внедрять его напрямую в игру. Вместо этого ключ хранится на моем сервере, который и отправляет запросы API. Лаунчер напрямую с API не взаимодействует — он общается только с сервером. Пользователь может по желанию авторизоваться в лаунчере, используя логин и токен GameJolt — токен был специально придуман, чтобы не сообщать играм пароль от аккаунта. В случае успешной авторизации достижения в игре будут синхронизироваться с аккаунтом.

Лаунчер также позволяет пользователю настроить графику игры перед запуском. Для этого он взаимодействует с конфигурационным файлом игры — settings.conf. Лаунчер считывает текущие настройки, обновляет их в зависимости от выбранных пользователем параметров и сохраняет изменения обратно в файл. Также он мониторит изменения в конфиге — если игра обновила файл, интерфейс лаунчера подхватит изменения.

На будущее есть план реализовать систему автообновления (полезно для игроков, которые не используют игровые платформы и предпочитают устанавливать игры вручную). Кроме того, можно добавить установку модов.

BT — тулчейн баннерной разработки

Ранее я обещал рассказать о своем новом проекте, не связанном с D — выполняю обещание. Волею судеб несколько лет назад я стал профессиональным баннермейкером. Это разновидность фронтенд-разработки на стыке с анимационным дизайном — чаще всего я делаю HTML-баннеры и виджеты с разнообразной анимацией, эффектами и интерактивными механиками. Поскольку почти все рекламные сети имеют ограничение для баннеров по весу (и довольно серьезное — обычно 150 кб), эта профессия еще и пересекается в какой-то степени с демосценой, где на первом месте — искусство минимизировать информацию и генерировать ее процедурно.

Баннеры — это обычные HTML-странички, которые показывают пользователю рекламный сюжет и содержат ссылку на сайт рекламодателя. Они могут быть изготовлены при помощи самых разных инструментов, в том числе визуальных (Adobe Animate, Google Web Designer), но самое гибкое решение — писать непосредственно на HTML и JavaScript, рисуя графику либо обычными элементами DOM, либо через canvas. Благодаря отказу от Animate, вы не привязаны к его JS-библиотеке, которая сама по себе сжирает много веса, если ее нужно приложить к баннеру локально. Однако для отрисовки мало-мальски сложной анимации одним CSS вы не обойдетесь, и вам нужны такие библиотеки, как GSAP, Anime.js и др. — благо, весят они совсем немного. Основная сложность — уместить все ресурсы баннера в те самые 150 кб, что порой представляет нетривиальную задачу.

Для решения этой и многих других задач, которые возникают при разработке HTML-баннеров, я написал на Node.js комплект инструментов BT (Banner Toolchain). Его идея уходит корнями во внутренний инструментарий компании SmartHead, которым я пользовался три года и решил переделать полностью с нуля, уже в качестве независимого проекта с более эффективной реализацией большинства фич.

BT — это локальный сервер, предназначенный для разработки одностраничных сайтов с упором на минимизацию веса всех ресурсов. Например, в нем есть встроенный оптимизатор изображений, генератор CSS-анимации, функция записи баннера в GIF и видео, а также сборщик для подготовки баннера под все популярные рекламные площадки и упаковки в архив.

(далее…)

Веб-мусор

Когда я захожу на какой-нибудь сайт и вижу что-то из перечисленного ниже, у меня пропадает всякое желание заходить второй раз. Большая просьба ко всем вебмастерам: смело используйте эти элементы, если вы хотите отпугнуть посетителей и вызвать к себе тонны ненависти!

  • Реклама по площади больше, чем полезный контент, а также перекрывающая или блокирующая контент. И еще любая реклама со звуком, самопроизвольное перенаправление по партнерским ссылкам и открытие дополнительных вкладок/окон с рекламой.
  • Блокировка доступа к сайту при включенном Адблоке. Я еще не решил, полезен ли для меня ваш сайт, а вы уже хотите на мне заработать? Если реклама для вас жизненно важна, просто культурно сообщите об этом, и я отключу блокировщик рекламы — после того, как прочитаю то, что мне было нужно. Но если вы мне сразу не дадите это сделать, то я просто уйду и больше не вернусь.
  • Навязчивые всплывающие сообщения и модальные окна, а также любые посторонние элементы, самопроизвольно блокирующие или мешающие видеть контент — подписка в соцсетях, опросы и т.д. Если я захочу подписаться на вашу страницу в соцсети или пройти опрос, я это сделаю без вашего напоминания.
  • Блокировка копирования текста или правого клика на изображениях. Напрасно стараетесь — все равно и текст, и изображения легко вытащить из HTML. За кого вы принимаете своих посетителей, за полных идиотов?
  • Требование ввести капчу просто для доступа к сайту (не для регистрации или авторизации).
  • Игнорирование HTML5 (верстка, целиком выполненная на Flash, видео или аудио на Flash).
  • Автоматически воспроизводящаяся фоновая музыка, особенно если она не выключается.
  • Атоматически запускающееся видео — это даже хуже музыки, но, увы, в последнее время куда популярнее.
  • Тексты не в UTF-8. Я этих ужасов с кодировками наелся в нулевые.
  • DRM в любой форме. Если я не смогу скачать ваше аудио или видео и посмотреть его локально своим плеером (так, как удобно мне), то не обессудьте — пойду в другое место.
  • Бесконечные ленты, которые рано или поздно начинают подвешивать браузер. Пожалуйста, разбивайте длинные ленты на страницы.
  • Регистрация по номеру телефона, если это не мессенджер (хотя мне и в мессенджерах это не нравится), а также необоснованное требование при регистрации предоставить конфиденциальную информацию (полное имя, адрес проживания, паспортные данные и т.д).
  • Блокировка доступа к сайту из старых или малоизвестных браузеров. Не вам решать за меня, каким браузером пользоваться — могу вообще свой HTTP-клиент написать.
  • Черные списки IP, блокировка выходных узлов TOR и браузерных прокси.
  • Большие файлы без возможности докачки.

Пример работы с Yahoo! Finance на D

Пример работы с сетевым API Yahoo! Finance: запрос котировок ценных бумаг. Демонстрирует некоторые распространенные идиомы Phobos – например, форматированный ввод, объекты времени и даты. Использует минималистичный HTTP-клиент DHTTPClient. На ввод программа принимает тикер (биржевое обозначение акции) – например, MGNT.ME для акций “Магнит”. Выводит стоимость, дату и время последней сделки.

module main;

import std.stdio;
import std.string;
import std.uri;
import std.format;
import std.datetime;

import dhttpclient;

struct Quote
{
    string symbol;     // s
    string name;       // n
    double lastTrade;  // l1
    string currency;   // c4
    DateTime datetime; // d1 t1
}

Quote getQuote(string sym)
{   
    const request = "snl1c4d1t1";

    const url = 
        "http://finance.yahoo.com/d/quotes.csv?e=.csv"
      ~ "&f=" ~ request
      ~ "&s=" ~ sym.encode;
    
    auto http = new HTTPClient();
    auto data = http.get(url).chomp;

    Quote q;

    with (q)
    {
        uint year, month, day;
        string time;

        formattedRead(data, 
            ""%s","%s",%s,"%s","%s/%s/%s","%s"", 
            &symbol, &name,
            &lastTrade, &currency,
            &month, &day, &year, &time);

        uint hour, minute;
        formattedRead(time, "%s:%s", &hour, &minute);
        if (time[$-2..$] == "pm")
            hour += 12;

        datetime = DateTime(year, month, day, hour, minute);
    }

    return q;
}

void main(string[] args)
{
    string s = "AAPL"; // Apple Inc.
    
    if (args.length > 1)
        s = args[1];

    auto q = getQuote(s);

    writefln("Symbol: %s", q.symbol);
    writefln("Name: %s", q.name);
    writefln("Last trade: %s %s (%s)", 
        q.lastTrade, 
        q.currency, 
        q.datetime);
}