Прокачиваем NES Classic Mini

На geektimes.ru недавно была статья о том, что «умельцы» взломали NES Classic Mini. Однако, там даже не упомянули о том, что это сделали русские. Нет, не я, а человек под ником madmonkey. Я же сразу решил написать приложение под Windows с дружелюбным интерфейсом, чтобы это можно было делать в пару кликов. В этой статье я хочу рассказать более детально о сути «взлома», о том, как в NES Mini всё устроено, и о трудностях, с которыми пришлось столкнуться.

Введение

NES Classic Mini. Она же NES Classic Edition в США. Весьма спорная штука. С одной стороны это просто эмулятор на линуксе, который представляет разве что коллекционную ценность, и гораздо лучше купить какую-нибудь Raspberry-Pi. Даже официальный сайт нам говорит, что это в первую очередь просто хороший подарок для коллекционеров и игроков, ничего более.

С другой стороны это всё-таки готовое решение, которое имеет привычный дизайн, оригинальные контроллеры и работает прямо из коробки на современных телевизорах, что ещё нужно рядовому пользователю?

Споров на эту тему много, но с одним согласны все — встроенных тридцати игр явно мало. Пусть там собраны и самые хиты, но очень уж не хватает возможности добавлять или, что было бы логично, докупать другие игры. Особенно для жителей России, где в 90е царило пиратство, и популярны у нас были совсем другие игры. Пираты не завозили к нам многие хиты, зато познакомили нас, например, с Battle City, которая за пределами Японии официально не выходила, но в России стала наверное даже более узнаваемой, чем Марио.

После выхода консоли было много попыток исправить эту ситуацию, я тоже пытался это сделать, но моих мозгов и знаний Linux явно не хватало. Да, я разобрал консоль и подпаялся к UART выводам, но туда вещает только загрузчик консоли Linux там нет. Да, по инструкциям из Интернета я смог запустить на ней собственную сборку Linux, но доступа к внутренней NAND-памяти таким образом я не получил.

Так и пылилась у меня эта консоль без дела, пока в один прекрасный день человек под ником madmonkey не взломал NES Mini. Самое забавное, что для взлома не нужно ничего ни паять, ни разбирать. Всё делается через подключение консоли через micro-USB разъём, который предназначается для питания. Делается это самым обычным micro-USB кабелем, который идёт в комплекте. Дело в том, что NES Mini построена на основе процессора от Allwinner, а у них предусмотрен так называемый FEL режим, который используется для отладки и прошивки по USB. На разных устройствах в этот режим можно перейти по-разному, и в случае с NES Mini нужно просто зажать кнопку RESET во время включения. Сделав это, можно с помощью специальной программы читать оперативную память, писать её и запускать код на выполнение.

Как работает «взлом»?

Грубо говоря, защиты-то никакой и нет, если не считать тот факт, что большая часть NAND зашифрована, но ключ при этом лежит в открытом виде в незашифрованной части памяти. При этом на официальном сайте Nintendo можно найти исходные коды загрузчика. Да, в соответствии с лицензией GPL Nintendo обязана выкладывать исходные коды своих разработок. Так madmonkey смог скомпилировать загрузчик, загрузить его в оперативную память NES Mini, запустить на выполнение, прочитать ядро Linux вместе с образом RAM-диска и точно таким же способом записать его назад. Дальше дело техники. Madmonkey набросал программу под названием “hakchi”, которая позволяет прочитать ядро Linux, пропатчить его и зашить обратно, в итоге появляется возможность добавлять свои игры.

Мне сразу же стало интересно разобраться, как это работает, чтобы научиться чему-то новому и, быть может, как-то усовершенствовать. Но это оказалось не так просто. Я наверное целый день потратил, изучая его скрипты, при этом огрызался и рычал на родных и близких при малейшей попытке меня отвлечь. Но в конце концов разобрался. Для меня вот стало открытием, что в Linux можно монтировать одну директорию поверх другой. Именно монтировать, а не создавать симлинки. Игры находятся на разделе, который доступен только для чтения. Но при этом у NES Mini есть относительно огромный раздел размером в 300 мегабайт, который доступен для записи, в нём она хранит сохранения игр и настройки. Скрипты madmonkey создают на этом разделе директорию под игры, копируют туда содержимое оригинальной директории и монтируют нашу директорию поверх оригинальной на раннем этапе загрузки системы. В итоге папка с играми становится доступна для записи, а оригинальные файлы остаются в целости и сохранности, что немаловажно, и оболочка не замечает, что директорию подменили. Аналогично можно подменить и системные файлы, открыв доступ к консоли через UART, что наконец-то позволяет взглянуть на всю файловую систему и облегчает отладку.

Что внутри?

Вскоре я написал простенькую программу, которая через hexdump по UART проводу выкачивает всю файловую систему. Это весьма медленный процесс, но очень уж хотелось посмотреть, что там есть.

Как я уже сказал, игры лежат в специальной директории, для каждой игры там отдельная поддиректория с обложкой, конфигом и собственно ROM’ом игры. Самое забавное, что ROM’ы эти в обычном «.nes» формате, и они ни на байт не отличаются от тех ROMов, которые легко можно найти в Интернете. Вы скажете: “Ну и что? Игры же одинаковые, с чего им отличаться?”

Да дело в том, что формат iNES был разработан пиратами, которые первыми начали дампить картриджи и писать эмуляторы. В заголовке iNES файлов используется нумерация мапперов, которая тоже была разработана пиратами. Тут я выпал в осадок. Нет, я конечно понимаю, что Nintendo проще скачать свои же игры из Интернета, чем искать где-то у себя в архивах прошивки картриджей 30-летней давности. Но я совершенно не думал, что они так и поступят. Получается, Nintendo продаёт нам пиратские копии своих же собственных игр? Вскоре мне рассказали, что это практиковалось ещё на Nintendo Wii. Да и в новостях оказывается есть уже небольшая шумиха об этом.

Как бы там ни было, нам это только на руку. Если NES Mini использует пиратский формат хранения игр, значит на ней можно запускать пиратские копии игр из Интернета без особых модификаций. Это, опять же, сделал madmonkey. Я ещё недоумевал — как же NES Mini определяет маппер игры. Мне тогда и в голову не могло прийти, что из пиратских заголовков.

Разработка hakchi2

Но вернёмся к тому, что я решил написать простую и удобную программу для закачки игр в NES Mini. Аналог той, что сделал madmonkey, но такую, чтобы, простите за каламбур, даже обезьяна могла разобраться. С названиями у меня очень туго, и раз его программа называется hakchi, то пусть моя называется hakchi2. Как бы странно это ни было. Идеальный интерфейс я видел для себя так: слева должен быть список игр с галочками, справа — настройки выделенной игры. Внизу кнопки — добавить игр и прошить. Так нажать куда-то не туда или запутаться будет просто невозможно.

Что же касается того, что у программы «под капотом», она выполняет три основных действия:

  • Дампит ядро Linux из NES Mini. Для этого через FEL-режим в оперативную память консоли загружается u-boot, ему даётся команда на чтение данных из NAND памяти опять же в оперативную память, а затем с помощью того же FEL ядро читается оттуда на компьютер.
  • Прошивает в NES Mini модифицированное ядро. Модификация нам нужна только одна — чтобы при загрузке запускался наш скрипт, если он существует. Соответственно это действие нужно выполнить всего один раз. Процесс записи ядра в NAND память выполняется по тому же принципу, что и чтение, только наоборот.
  • Собирает модифицированное ядро со скриптами и играми, но не прошивает его, а загружает в оперативную память и выполняет. Скрипты при этом монтируют нужные разделы, записывают новые игры, после чего выключают консоль.

Для разборки и сборки ядра я как и madmonkey использую уже готовые программы из набора Android Kitchen. То есть просто запускаю чужие исполняемые файлы с нужными параметрами и в нужном порядке.

А вот сам процесс чтения и записи памяти несколько сложнее, и тут лучше работать напрямую. Есть готовые библиотеки, но на Си, а я пишу на C#. Конечно можно было написать враппер для unmanaged кода, но я погуглил описание FEL протокола и решил написать свою собственную библиотеку с нуля. Ничего сложного там нет, и оно вскоре даже заработало, к моему удивлению.

Для работы с ROMами я использовал уже готовые свои же библиотеки. Опытным путём выяснил, какие мапперы поддерживает встроенный в NES Mini эмулятор, чтобы программа сразу отсеивала игры, которые не запустятся. Кстати, если не знаете, что обозначает слово «маппер» в данном контексте, почитайте мою статью про дампинг картриджей NES/Famicom/Dendy.

Затем я сделал возможность менять играм различные параметры, добавил возможность выбирать обложки и автоматически их пережимать, добавил кнопку для автоматического поиска обложек в Google, в последний момент решил всё-таки добавить ещё и русский язык помимо моего кривого английского. И вот в таком виде выложил всё это дело в сеть, назвав версией 2.0. Потому что «hakchi2».

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

Наиболее интересные проблемы и задачи

Windows и установка драйверов

Самой сложной проблемой на начальном этапе для меня стала установка драйвера. Если уж я решился сделать программу простой, драйвер должен устанавливаться максимально легко. Мне совсем не хотелось давать пользователям сложные инструкции или посылать их на какой-то сайт качать отдельную утилиту. Я говорю про Zadig.

image

Это отличное приложение для простой и быстрой установки популярных базовых USB драйверов, в нашем случае это WinUSB. Кстати говоря, не понимаю, почему пользователю нужно выполнять кучу сложных действий, и Windows требует от разработчика цифровой подписи, когда нужно установить драйвер непосредственно от Microsoft. К счастью, у Zadig открытый исходный код, да ещё и с консольной версией в примерах. Я быстро сделал из неё простенькую программу, которая при запуске сразу ставит драйвер.

Тонкости FEL протокола

Стоит ли говорить, сколько багов всплыло в первое время… Больше всего я промучился с ошибкой “pipe read error”, которая возникала в момент, когда не получалось инициализировать устройство после запуска кода в памяти на исполнение. Однако, возникала она не каждый раз, а абсолютно случайно, из-за чего я много раз ошибочно считал, что наконец-то нашёл хоть какую-то закономерность. Но нет, ошибка возникала абсолютно случайно. И больше всего меня напрягло то, что если в момент, когда NES Mini переставала отвечать моей программе, запустить оригинальную hakchi от madmonkey, то консоль выходила из ступора и продолжала работать. То есть madmonkey у себя каким-то образом правильно проводит инициализацию, а у меня что-то неправильно. Но сколько я не изучал его исходники, ничего особенного я там не увидел. В итоге я нашёл программу, которая перехватывает и показывает USB-трафик и начал сравнивать всё побайтово.

Вот пример того, как должна проводиться инициализация/верификация:

У меня возникала проблема именно на втором шаге, при получении ответа. Почему-то приходили совсем не те данные, что я ожидал. Оказалось, что моя ошибка была в том, что в этом случае я пытался выполнить инициализацию заново, с самого начала. Программа от madmonkey (точнее библиотека fel_lib) же в таком случае повторяет, начиная сразу со второго шага, после чего устройство начинает нормально отвечать. Шаманство какое-то, но ошибка исчезла навсегда.

«LED-bug», как прозвали его иностранцы

Однако, помимо этого я столкнулся с гораздо более странным багом. Наверное это самый странный и неочевидый баг за всю мою жизнь. Скрипты для копирования игр по окончании процесса выключают консоль, поэтому пользователю надо дождаться, пока погаснет светодиод. Но очень многие люди жаловались, что светодиод не гаснет даже через полчаса. Люди на форумах делились своим опытом. У кого-то всё идеально работает, а у кого-то светодиод не гаснет. Кто-то считает, что существуют разные версии консолей, у кого-то всё начинало работать на другом компьютере. Десятки людей пытались найти хоть какую-то закономерность. Опять же было много разных иллюзий, но в конце концов нашёлся человек, который нашёл 100% верную закономерность. Поначалу я не поверил, но все в один голос начали это подтверждать.

Моя программа не работает, если её распаковать WinRAR’ом, но работает, если распаковать 7zip’ом. Как такое может быть?

Оказывается, некоторые версии WinRAR’а при определённых условиях не сохраняют атрибуты файлов при распаковке, а когда мы под Windows собираем RAM-диск для ядра Linux, симлинки обязательно должны иметь атрибут “системный”. Самому мне даже в голову не могло прийти то, что проблема в архиваторе, тем более я сам WinRAR’ом пользуюсь. Вскоре я добавил в программу проверку атрибут файлов, и проблема исчезла навсегда. Правда, под Windows 10 иногда почему-то не получается поменять атрибуты, но теперь об этом хотя бы пишется ошибка.

Шрифты

На этом этапе программа стала уже вполне стабильной, но был ещё целый ряд проблем, которые касались самой консоли и её оболочки.

Начать я решил со шрифтов. Проблема в том, что оригинальные шрифты в NES Mini содержат только нужные символы, и названия многих добавленных игр выводились некорректно.

Эта задача казалась вполне решаемой с первого взгляда, ведь прямо в директории с играми лежат файлы «title.fnt» и «copyright.fnt», и надо просто их отредактировать или заменить. Однако, ни один редактор шрифтов не согласился их открывать, нужно как-то понять, что это за формат.

Если поменять эти файлы местами, то текст в названии игр становится маленького размера.

Выходит, шрифт растровый, а не векторный, и в нём символы содержатся в виде рисунков. Если открыть файл в шестнадцатеричном редакторе, то можно увидеть, что каждый шрифт недалеко от начала содержит сигнатуру “BMF”.

Гугление по запросу “BMF font” привело на сайт, где была как утилита для генерации шрифтов, так и подробное описание формата, которое я сразу же бросился читать. Да, каждый файл действительно должен содержать сигнатуру BMF, но в самом начале файла. В случае же NES Mini перед ним шли ещё какие-то 9 байт, в разных файлах они были разными (кроме первого байта). Я понадеялся, что они не нужны или несущественны, но при изменении любого из них, консоль просто не запускалась, демонстрируя чёрный экран. Выходит, что надо обязательно понять смысл этих девяти байт. Первый — всегда единица. Затем два байта — это какие-то значения, потом два нуля. Снова два байта — значения и снова — два нуля. После этого уже шли данные шрифта. Я сразу же подумал, что эти пары похожи на два 32-битных числа. Посмотрел первое, сравнил с размером файла, никаких закономерностей не увидел. Аналогично со вторым, но затем я решил их сложить и получил точный размер файла без этого заголовка. Выходит, эти числа говорят нам о размерах каких-то секций в файле. Я отмотал файл на значение указанное в первых четырёх байтах и увидел заголовок PNG файла.

Я вытащил его оттуда и да, это картинка со всеми символами.

Логично, ведь программа для генерации шрифтов даёт на выходе несколько файлов. У NES Mini же они просто объединены в один. Я аналогично собрал вместе заголовок и файлы сгенерированного шрифта, скинул результат на NES Mini и недостающие символы появились.

Казалось бы, теперь все теперь должны быть довольны, но скоро мне начали писать японские владельцы Famicom Mini, жаловались на то, что у них пропали все иероглифы. Я им вежливо объяснил, что с японским языком у меня плохо, точнее совсем никак. Но не поленился рассказать, что я выяснил, и как самим сгенерировать шрифт. Вскоре мне прислали японский шрифт, и я включил его в дистрибутив.

Скрипты без проблем определяют регион консоли, чтобы выбрать нужный из двух шрифтов. Спасибо японцу под ником xsnake. Сейчас народ уже разобрался и начал активно выкладывать самые разные шрифты, даже Comic Sans есть, куда же без него.

Модификация драйвера игрового контроллера

Люди продолжали просить какой-то нереальный функционал. Многим не хватало возможности нажать кнопку RESET, то есть выйти в меню, не выпуская контроллера из рук. Я сразу же сказал, что это невозможно. Исходников эмулятора у меня нет, поменять функциональность кнопок нет возможности, но вскоре я осознал, что если подключить Classic Controller от Wii (они совместимы), на котором больше кнопок, то кнопка HOME работает именно как выход в меню. То есть в коде эмулятора это предусмотрено. При этом эмулятор использует библиотеку SDL2, у которой открытый исходный код, но пересобирать и заменять такую огромную библиотеку ради такой простой функции как-то не круто. Я опять начал смотреть в исходники, которые предоставляет сама Nintendo, и увидел там исходный код драйвера контроллера. Да это же именно то, что нужно! Внутреннее кодовое название контроллера, кстати, “Clovercon”. От слова “clover” (клевер). Аналогично называется и оболочка на NES Min — Clover, а название самой модели консоли — CLV-001. Думаю, теперь всем понятно, что значит это “CLV”.

Код драйвера весьма простой, и я быстро нашёл, куда вставить всего одну строку:

C
if (down && select) home = 1;

Скомпилировал драйвер я без особых проблем, что удивительно, ведь с Linux’ом я плохо дружу, а тут вдруг скомпилировал модуль ядра, но обрадовался я рано. Утилита insmod отказывалась загружать этот модуль. После недолгого гугления я понял, что это из-за того, что не совпадает «vermagic». Это строка внутри модуля, которая описывает версию ядра Linux и параметры, с которыми оно собиралось. Делается это банально для того, чтобы убедиться в бинарной совместимости. Короче говоря, собирать драйвер нужно с теми же параметрами ядра, при которых собиралось ядро NES Mini. А откуда мне их знать? Да, Nintendo выложила на своём сайте и исходники ядра, но там нет файла с настройками. Я долго мучился, меняя самые разные параметры ядра, из строки vermagic было примерно ясно, чего не хватает, или что лишнее.

Однако когда строки vermagic совпали и модуль загрузился, система отказывалась реагировать на нажатия кнопок. При этом отладить его было невозможно, т.к. kprint в ядре NES Mini был вырезан, как и буфер dmesg. В итоге я уже почти сдался, потеряв всякую надежду, но залез в раздел “Kernel hacking” и начал снимать все галочки подряд.

Опытные линуксоиды меня наверное засмеют, но в итоге внезапно драйвер заработал. Я своего добился, комбинация down+select стала открывать меню.

Правда, люди вскоре начали просить сделать возможность выбирать комбинацию кнопок. Казалось бы, как? Ведь они зашиты в бинарник драйвера, но я просто добавил в код текстовую строку, из которой драйвер берёт комбинацию, а перед загрузкой в консоль моя программа находит эту строку и меняет значения.

C
volatile char MAGIC_BUTTONS[] = "MAGIC_BUTTONS:00100100";

Главное — не забыть про директиву «volatile», чтобы компилятор понимал, что строка может меняться «из вне», и что вырезать код её проверки не нужно.

Всё это имело бы огромный смысл, если бы Nintendo не сделала такой короткий провод у контроллеров. Удлинитель теперь просто необходим.

Вскоре появились и люди, которые начали просить турбо-кнопки. Я всегда считал их читерством, к которому нас приучили с детства, ведь в России оригинальные контроллеры практически никто никогда не видел. И я игнорировал эти просьбы, пока они не начали поступать от иностранцев. Думаю, тут рассказывать уже особо нечего, просто очередная модификация драйвера. Теперь можно на секунду зажать select+A или select+B, чтобы включить турбо на соответствующей кнопке. В случае же с Classic Controller’ом кнопки X и Y сразу же работают как турбо A и турбо B.

Преодоление ограничений

Что же касается ограничения на количество игр, тут всё не совсем понятно. Дело в том, что в NES Mini без проблем можно залить примерно 97 игр, но при этом перестают работать сохранения. И чем меньше игр в меню, тем больше сохранений можно сделать, но дело вовсе не в ограничениях размера flash-памяти, места на разделе ещё очень много. Похоже, что оболочка не может или не пытается получить столько оперативной памяти, чтобы загрузить все картинки, ведь каждая сохранённая игра сопровождается скриншотом, и если посчитать общее количество игр, размер их обложек, размер скриншотов и учесть, что в памяти всё это хранится скорее всего в несжатом виде, получается весьма большое число.

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

Так ведь можно запустить не только эмулятор, но и любой скрипт, что не мешает запустить скрипт, который будет монтировать другую директорию с играми, что для пользователя будет выглядеть как папки. Попробовал — получилось!

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

Интересные особенности

За этот месяц было добавлено и много других разных функций вроде поддержки Game Genie чит-кодов и автоматического заполнения информации об играх, сейчас уже всё и не упомнить.
Было найдено и много багов, интересных особенностей.

Например, консоль отказывается запускаться, если среди игр есть хоть одна, в названии которой подряд идут апостроф и любая цифра. Пришлось ввести проверку на это.

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

Кстати, у встроенного эмулятора много и других параметров, он охотно сам их выводит. Правда, почему-то они не все работают. Например, эмуляцию PAL не получается включить при всём желании. И да, европейская версия консоли содержит американские версии игр. И BIOS для Famicom Disk System там тоже есть, хоть игры для неё и выходили только в Японии. Так что они тоже запускаются.

Из картриджных же игр поддерживается не очень много мапперов, но все самые популярные на месте:

  • 0 (NROM) — простейшие игры без маппера, например Ice Climber, Pac-Man, и т.п.
  • 1 (MMC1) — много хороших игр, второй по популярности маппер.
  • 2 (UxROM — UNROM/UOROM) — игры вроде Castlevania, Contra, Duck Tales и т.п.
  • 3 (CNROM) — много простых игр, но с большим объёмом графики
  • 4 (MMC3) — самый популярный маппер, очень много игр
  • 5 (MMC5) — очень сложный и самый навороченный маппер, удивительно, что есть его поддержка, ведь вроде под него нет ни одной игры в стандартном наборе
  • 7 (AxROM — ANROM/AMROM/etc.) — простой маппер, который используется играми вроде Battletoads.
  • 9 (MMC2) — используется только игрой Punch Out!!
  • 10 (MMC4) — используется только несколькими японскими играми
  • 86 — редкий маппер, мало где используется
  • 87 — редкий маппер, мало где используется
  • 184 — редкий маппер, мало где используется

Однако, меня продолжают заваливать письмами с просьбами добавить в программу поддержку того или иного маппера, не понимая, что это не от меня зависит. Хотя в теории вполне возможно скомпилировать под NES Mini другой эмулятор, но я оставлю эту затею людям поумнее.

Среди оригинальных игр есть одна скрытая. Точнее это не игра, а производственные тесты, что-то вроде сервисного меню. Быть может, какой-то хитрой комбинацией можно получить к нему доступ без танцев с бубном.

CaH4e3 (известный в определённых кругах ромхакер) уже начал дизассемблировать файл эмулятора. Забавный факт — в нём спрятано сообщение от разработчиков. Точнее от некого капитана Ханафуда.

На самом деле ханафуда — это игральные карты, которые Nintendo выпускала в позапрошлом веке. Санчез говорит, что на этот текст есть указатели, то есть какой-то код его использует. Вполне возможно, что это рабочая пасхалка.

Ещё из забавного: если в директории с любой игрой создать папку “pixelart” и положить туда любую PNG картинку, она будет показываться на фоне во время простоя консоли. Тут лучше посмотреть видео из начала статьи, чтобы понять, о чём речь.

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

Итоги

Пожалуй, это первый взлом консоли, который приносит издателям прибыль, а не убытки. Ведь, игры отдельно они не продают, а саму консоль теперь просто сметают с полок магазинов. Такое впечатление, что в Nintendo сидели и с нетерпением ждали, когда же её взломают уже, поэтому и не делали никаких защиты. Так что надеюсь, они не поставят мне страйк. И не засудят, тем более незаконного я вроде как ничего не делаю, пока не распространяю игры.

upd: Самое главное-то и забыл. Вот ссылка на hakchi2 и её исходный код:
https://github.com/ClusterM/hakchi2

Добавить комментарий