wotapi · PyPI

wotapi · PyPI World of Tanks
Содержание
  1. Начало (протокол)
  2. Description
  3. Install
  4. Usage
  5. Left To Do
  6. Api reference | developer room
  7. Api versions
  8. Available api¶
  9. Coverage tests
  10. Developer room
  11. Developers partner program
  12. Etag header field
  13. Github — -public-api-client
  14. Global map
  15. Nuget или необходимость вручную добавлять библиотеку json.net
  16. Parameters conversion¶
  17. Registering application
  18. Response format
  19. Tokens:
  20. Unittesting
  21. Usage and common things¶
  22. Parameters conversion¶
  23. Using application_id
  24. Wg api для ts3
  25. World of tanks api
  26. Анализ данных
  27. Браузер
  28. Важность признаков
  29. Внутреннее взаимодействие компонентов «позади» api
  30. Возврат пользователя
  31. Выбор проекта для генерации клиента
  32. Генерация ссылки для перенаправления пользователя
  33. Гибкое кэширование данных
  34. Динамический анализ
  35. Зависимость от html
  36. Инструменты которые нам понадобятся
  37. Интеграционное тестирование
  38. Использованные сторонние решения
  39. История развития публичных api
  40. Как это работает?
  41. Качество кода
  42. Конкурс
  43. Максимальное покрытие кода тестами
  44. Немного статистики
  45. Необходимость ручного допиливания напильником
  46. Неоптимальное api клиента
  47. Нормализация признаков
  48. Пакеты (ассиметричное шифрование)
  49. Перенаправление пользователя на сайт wg
  50. Плохой интернет
  51. Поддержка «частичных» ответов
  52. Покрытие тестами
  53. Предпосылки создания wargaming public api
  54. Приложения
  55. Пример вызова такой функции (caller)
  56. Пример использования:
  57. Принципы взаимодействия
  58. Проверка данных, которые получили после возврата пользователя
  59. Работа с признаками
  60. Разработка
  61. Реализация:
  62. Резюме
  63. Создание и отбор признаков
  64. Теория
  65. Тест модели
  66. Требования
  67. Удобный способ для обработки цепочек запросов
  68. Удобный способ интеграции в приложения
  69. Шифрование (симметричное шифрование)
  70. Заключение

Начало (протокол)

Начинать анализ я начал с определения протокола который использует игра для коммуникации (TCP / UDP).

Открываем procmon (делаем попытку авторизации в клиенте игры).

Сетевая активность показывает что в игре используется UDP протокол (на момент авторизации точно)Я заметил пакеты размером 12 байт (сообщения ping / pong), но что самое интересное, это пакет размером 273 байта и ответ размером 30 байтМожно смело сказать что этот пакет является пакетом авторизации.

Description

This package will extract data from the Wargaming World of Tanks API.Currently, this works only for the PC version with the rest of the platforms to be implemented in future iterations.

The package will require the following from the official World of Tanks Developer API page.

  • application id
  • account id
  • access token

All data extracted will be written to a local sqlite database ready to be accessed. The database is automatically created
at the location where the script is executed.The name of the database is world_of_tanks.db of type sqlite.

Install

pip install WotAPI

Usage

from worldoftanks import WotAPI

wot = WotAPI(application_id='############',
             account_id='##########',
             token='#########',
             realm='eu')
# Extract Account Data
wot.player_personal()
wot.player_vehicles()
wot.player_achievements()

# Extract Tankopedia Data
wot.tankopedia_vehicles(load_once=True)
wot.tankopedia_achievements(load_once=True)
wot.tankopedia_information(load_once=True)
wot.tankopedia_maps(load_once=True)
wot.tankopedia_badges(load_once=True)

# Extract Player Vehicles Data
wot.vehicle_achievements()
wot.vehicle_statistics()

All data from the Tankopedia part of the API needs to be loaded only once in the database, otherwise this will be duplicated.
For ease, the argument load_once is by default set to True.

The data can be accessed from the wot objects for further development. The response is a list of dictionaries.

Left To Do

API PartNameDate CompletedVersion
AccountsPlayer Personal Data2020-04-240.0.1
AccountsPlayer Vehicles2020-04-240.0.1
AccountsPlayer Achievements2020-04-240.0.1
TankopediaVehicles2020-04-250.0.2
TankopediaAchievements2020-04-250.0.2
TankopediaTankopedia Information2020-04-250.0.2
TankopediaMaps2020-04-250.0.2
TankopediaBadges2020-04-280.4.22
TankopediaVehicle characteristics
TankopediaEnginesDeprecated
TankopediaTurretsDeprecated
TankopediaRadiosDeprecated
TankopediaSuspensionsDeprecated
TankopediaGunsDeprecated
TankopediaEquipment and Consumables
TankopediaPersonal Missions
TankopediaPersonal Reserves
TankopediaVehicle Configurations
TankopediaModules
TankopediaCrew Qualifications
TankopediaCrew Skills
VehiclesVehicle statistics2020-04-270.3.2
VehiclesVehicle achievements2020-04-270.3.2
ClansClans
ClansClan Details
ClansClan Member Details
ClansClan Glossary
ClansMessage Board
ClansPlayer Clan History
Clan ratingsTypes of Ratings
Clan ratingsDates with available r.
Clan ratingsClan Ratings
Clan ratingsAdj Positions In Clan R.
Clan ratingsTop Clans
Strongholds
Global Map

Api reference | developer room

Api versions

MethodAccepted API version
Searching players1.0, 1.1
Searching clans1.0, 1.1
Showing player stats1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8
Personal info1.0
Logging in1.0

Available api¶

importwargaming# World of Tankswot=wargaming.WoT('demo',region='ru',language='ru')# Wargaming NETwgn=wargaming.WGN('demo',region='na',language='en')# World of Tanks Blitzwotb=wargaming.WoTB('demo',region='eu',language='pl')# World of Warshipswows=wargaming.WoWS('demo',region='eu',language='fr')# World of Warplaneswowp=wargaming.WoWP('demo',region='eu',language='en')# World of Tanks XBoxwot_xbox=wargaming.WoTX('demo',region='xbox',language='ru')# World of Tanks Playstation 4wot_ps4=wargaming.WoTX('demo',region='ps4',language='ru')

Coverage tests

coverage run --source=worldoftanks -m unittest discover -s worldoftanks/tests
coverage report -m

Developer room

Developer Room provides necessary information to develop your own services and applications using the API methods.

To log in to Developer Room, use your Wargaming.net account.

Developers partner program

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

В рамках этого проекта мы также хотели бы собрать разработчиков игровых

и попытаться наладить с ними конструктивный диалог. Если API к данным сейчас закрывает многие потребности разработчиков, то у мододелов жизнь сильно сложнее. Надеюсь, нам удастся сделать ее немного приятнее. Собственно, именно поэтому весь проект целиком называется Wargaming Developers Partner Program, а не просто Public API.

Etag header field

Response header may contain ETag header field, that allows a client to perform conditional requests. Conditional GET request asks the server for a document considering a specific parameter to reduce data transfer overhead. See w3 documentation.

Github — -public-api-client

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

Данный класс организован так что бы функционал, который доступен будет генерируется сам. При модификации определенных переменных будет сгененрирован код для API World of Tanks, World of Warplanes, Wargaming.NET

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

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

Получить ключи от приложений возможно на

При инициализации нужно обезательно указать ключевые данные:

Что бы регенерировать код нужно указать для:

World of Tanks

World of Warplanes

Учитывайте что команда авторизации присутствует только в API World of Tanks

Wargaming.NET

Global map

Region 1 (Northern Europe)

Nuget или необходимость вручную добавлять библиотеку json.net

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

Решение с Nuget пакетом могло бы избавить от необходимости копировать исходный код и подключать вручную Json.NET, но как уже было сказано выше, местами придется допиливать напильником.

Parameters conversion¶

All parameters to endpoint functions should be a keyword arguments. Arguments values are converted to the required format automatically.

Value typeConverted value typeExample
list, tuplestring[1,2,3]'1,2,3'
datetime.datetimestring (ISO format)datetime.datetime(2022,11,29,12,34,56)'2022-11-29T12:34:56'

Registering application

Once you logged in to Developer Room, to start using API, you need to register your application.

To register an application:

Response format

The API methods always return data in JSON format.

The response format has the following fields:

Tokens:

  • WG-WoT_Assistant-1.1.2
  • WG-WoT_Assistant-1.2.2
  • WG-WoT_Assistant-1.3.2
  • Intellect_Soft-WoT_Mobile-site
  • Intellect_Soft-WoT_Mobile
  • WG-WoT_Assistant-test
  • Intellect_Soft-WoT_Mobile-unofficial_stats

Unittesting

For development purposes, the unittests can be executed via:

python3 -m unittest discover -v worldoftanks/tests

Usage and common things¶

Parameters conversion

All parameters to endpoint functions should be a keyword arguments. Arguments values are converted to the required format automatically.

Value typeConverted value typeExample
list, tuplestring[1,2,3]'1,2,3'
datetime.datetimestring (ISO format)datetime.datetime(2022,11,29,12,34,56)'2022-11-29T12:34:56'

Using application_id

application_id is an application identification key used to send requests to API. The access to Wargaming.net content is granted only if your application has application_id.

The API always returns data in JSON format.

The request per second limit is set for each application_id.

Wg api для ts3

World of tanks api

http://worldoftanks.eu/community/accounts/500445118/api/1.9/?source_token=WG-WoT_Assistant-test

  {
    "spotted": 0, 
    "localized_name": "PzKpfw VI Tiger", 
    "name": "PzVI", 
    "level": 7, 
    "damageDealt": 0, 
    "survivedBattles": 0, 
    "battle_count": 223, 
    "nation": "germany", 
    "image_url": "/static/2.1.3/encyclopedia/tankopedia/vehicle/small/germany-pzvi.png", 
    "frags": 0, 
    "win_count": 105, 
    "class": "heavyTank"
  },

In eu server this api isn’t showing damage, spooted, frags and survived battle, but in all others are…

Про WoT:  Прохождение реферальной программы World of Tanks – 10 сезон. Инвайт WoT

Анализ данных

Зависит ли винрейт от нации техники? Логично предположить, что нет, так как разработчики пытаются максимально сбалансировать это. Давайте построим график. Для построения графиков я использовал библиотеку seaborn:

sns.factorplot('nation','winrate', data=df_normalized,size=4,aspect=3)
sns.plt.title('Winrate from nation')

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

А как обстоят дела с классом техники, какой класс «нагибает»? Построим похожий график:

ax = sns.factorplot('type','winrate', data=df_normalized,size=5,aspect=3)
sns.plt.title('Winrate from type')

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

Далее мы не будем говорить об этих двух признаках, так как они не вносят особой пользы в модель на основе random forest.

Посмотрим на корреляцию выбранных раннее признаков и процента побед:

Выделяется сильная корреляция is_premium с winrate. Неужели премиумные танки намного лучше обычных? Не совсем так. Такая сильная зависимость скорее всего объясняется тем, что на премиумной технике играют опытные игроки, чтобы фармить серебро, так как у многих танков, покупаемых за золото, льготный уровень боёв, больше серебра за бой, возможность быстрой прокачки экипажа. Можно построить график и посмотреть, как распределён винрейт на премиумной и обычной технике:

facet = sns.FacetGrid(df_normalized, hue="is_premium",aspect=4)
facet.map(sns.kdeplot,'winrate',shade= True)
facet.set(xlim=(0.40, df_normalized['winrate'].max()))
facet.add_legend()
sns.plt.title('Winrate from premium')

Видно, что плотность распределения побед на обычной технике — это Гаусовское нормальное распределение со средним значением 49%. Плотность распределения побед на премиумной технике вытянута в сторону большего винрейта, среднее значение 52%, а дисперсия намного больше чем у обычной техники.

В игре всего 114 премиумных танка, а это 25% от общего количества. На гистограмме всех танков по проценту побед мы видели хвост справа. Давайте посмотрим, какие танки попали в него:

Получается 93% танков из хвоста — премиумные. Что интересно остальные 7% (2 из 31) это чешские танки.

Также из таблицы корреляции видно, что винрейт прямо пропорционален уровню танка. Рассмотрим подробнее на графике:

Легко объяснить такую картину. На первых двух уровнях техники такой маленький винрейт из-за того, что все начинающие игроки портят статистику танкам из-за отсутствия опыта. Также на первых уровнях больше шансов оказаться внизу списка. На 10 уровне наоборот, ты всегда в топе. Также на 9-10 уровне играть без премиум аккаунта убыточно, поэтому большинство людей там, играют с премиумом.

Из оставшихся признаков прямо пропорциональны винрейту: прочность, скорость вперёд, урон в минуту и броня. Обратно пропорциональны: скорость сведения, разброс орудия. Пока всё очевидно, но дальше видно, что максимальный урон и бронепробитие обратно пропорциональны проценту побед.

Это странно, ведь чем больше танк наносит урона, тем лучше. Так и есть. Если еще раз взглянуть на то, как я получал значения для максимального урона, можно догадаться в чем подвох. Я просто брал максимальные значения урона и бронепробития из всех возможных снарядов для топового орудия.

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

Браузер

Текущее решение построено на базе CefSharp что уже означает что решение будет работать только на Win32 платформе. Можно переписать с использованием библиотек CefSharp, чтобы получить кроссплатформенное решение. Опять таки переход на прсинг JSON позволит избавиться от зависимости CefSharp и сделать решение которое будет работать на Windows, Mac и Web.

Важность признаков

Теперь можно построить random forest на этих данных и посмотреть на результат. Random forest это один из самых распространённых алгоритмов машинного обучения, основанный на усреднении результатов множества разных деревьев решений. Этот алгоритм хорошо подходит для того чтобы узнать важность отдельных признаков:

Я пробовал разные параметры и признаки, но сильно уменьшить ошибку мне не удалось. Видно, что алгоритм в среднем ошибается в предсказаниях на 1.3% процента. А теперь посмотрим на важность признаков для этого леса:

importances = rf.feature_importances_
std = np.std([tree.feature_importances_ for tree in rf.estimators_], axis=0)
indices = np.argsort(importances)[::-1]
legends = []
for i in range(X.shape[1]):
    legends.append('%d.%s (%f)' % (i   1, X.columns[indices[i]], importances[indices[i]]))
plt.title('Feature importances')
bars = plt.bar(range(X.shape[1]), importances[indices], color='c', yerr=std[indices], align='center')
plt.xticks(range(X.shape[1]), range(1, X.shape[1]   1))
plt.xlim([-1, X.shape[1]])
plt.legend(bars, legends, fontsize=12)

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

Что будет, если мы уберем из выборки премиумные танки и обучим random forest с такими же параметрами? Результаты удобно представить на boxplot:

fig, ax1 = plt.subplots(figsize=(10, 6))
data = [score_with_premium, score_without_premim]
bp = plt.boxplot(data, notch=0, sym=' ', vert=1, whis=1.5)
ax1.set_title('Comparison of score with and without premium')
ax1.set_ylabel('mean_absolute_error')
xtickNames = plt.setp(ax1, xticklabels=['With premium', 'Without premium'])
plt.setp(xtickNames, rotation=0, fontsize=12)

Алгоритму сразу стало намного легче угадывать процент побед и в среднем ошибка на кросс валидации уменьшилась до 0.9%, разброс ошибки также стал существенно меньше.

Внутреннее взаимодействие компонентов «позади» api

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

Вся наша инфраструктура вокруг игр построена по сервисной модели, то есть компоненты относительно независимы и взаимодействуют между собой через наборы API. Обусловлено это несколькими факторами:

  • даже если что-то падает, все остальное должно работать;
  • компонентов много (сервис аутентификации, OpenID-провайдер, система рейтингов, клановые сервисы, Мировая Война, Wargaming League, игровые порталы, форумы, энциклопедия, ЦПП и так далее — всего около 40 штук);
  • компоненты разрабатываются разными командами;
  • компоненты не релизятся все вместе;
  • полный простой системы при обновлении недопустим.

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

Возврат пользователя


После того как игрок авторизируется на сайте WG и разрешит нашему сайту просматривать его детальную статистику он будет перенаправлен на

?&status=ok&access_token=

&nickname=

&account_id=

&expires_at=


Если не произойдет никаких ошибок…

Таким образом наш скрипт получит данные:

status access_token nickname account_id expires_at

Но пока этим данным нельзя доверять!

Выбор проекта для генерации клиента


На текущий момент генерируется больше 40 тысяч строк для всех проектов сразу. Можно было бы добавить выбор каких проектов и каких методов этого проекта необходимо сгенерировать клиент.

На данный момент все проекты разделены namespace-ами и можно просто удалить проект и тем самым уменьшить конечный размер сборки.

То же самое касается лишних полей.

Можно бесконечно заниматься улучшением, но на этом можно подвести резюме.

Генерация ссылки для перенаправления пользователя

Прежде всего, нужно обратиться к методу auth/login с просьбой сгенерировать URL для дальнейшего редиректа пользователя.

Для этого шлем запрос к

Гибкое кэширование данных

There are only two hard things in Computer Science: cache invalidation and naming things.

— Phil Karlton


Под «гибким кэшированием» подразумевается следующее:


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

Мы пробовали несколько разных версий кэша — была у нас и
inMemory CoreData
, были попытки обыграть решение с использование
NSCache
. Позже мы решили, что кэш хоть и является важной фичей, но его реализация в любом из предложенных вариантов — весьма объемна в сравнении с размером всей остальной библиотеки. Поэтому мы перенесли всю функциональность кэша на уровень
NSURLConnectionNSURLCache
позволяет кэшировать ответы на запросы путем сопоставления
NSURLRequest
и соответствующего ответа. Хранить данные можно как на диске, так и в памяти.

Использование весьма лаконично:

NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:kInMemoryCacheSize
											         diskCapacity:kOnDiskCacheSize
												        diskPath:path];
[NSURLCache setSharedURLCache:urlCache];

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

У NSURLConnectionDelegate есть следующий метод, который позволяет нам немного «подправить» ответ перед его кэшированием:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;

Почему бы и нет?

Динамический анализ

Открываем наш любимый x64dbg и ставим 2 брекпоинта на экспортируемые функции ws2_32 [send / sendTo]

Отлично (это тот самый пакет который отправляется на сервер)Теперь нужно найти функцию шифрования которая шифрует данные авторизации. Изучив call stack, я нашел эту чудесную функцию которая принимает наш буфер и размер 0x100:

Тело сообщения это Json Объект.

К слову body имеет тоже свою структуру но углубляться в это я не буду.

Зависимость от html

Так как WG API не использует никаких стандартов описания JSON, приходится довольствоваться парсингом HTML описания. А HTML может меняться произвольным образом, а описание одних и тех же типов могут отличаться и встречаются даже на русском языке, т.е. нет никаких гарантий, что завтра не появится новое название используемого типа.

В следующей версии буду разбирать вместо HTML описания из JSON И влияние этой проблемы немного снизиться.

Инструменты которые нам понадобятся

  1. x64 dbg

  2. Cutter (Radare2)

  3. C 4. WireShark

Интеграционное тестирование

Код типичного теста представлен ниже:

	context(@"API works with players data", ^{
		it (@"should search players", ^{
			stubResponse(@"/wot/account/list", @"players-search.json");
			RACSignal *searchRequest = [client searchPlayers:@"garnett" limit:0];
			NSArray *response = [searchRequest asynchronousFirstOrDefault:nil success:NULL error:&error];

			[[error should] beNil];			
			[[response should] beKindOfClass:NSArray.class];
			[[response.lastObject should] beKindOfClass:WOTSearchPlayer.class];
			WOTSearchPlayer *player = response[0];
			[[player.ID should] equal:@"1785514"];
		});
    });

Замечу, что никаких плясок с асинхронностью/семафорами нет, так как у
RACSignal
есть замечательный метод специально для тестирования, который делает всю черную работу за программиста:

(id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error;

Использованные сторонние решения

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

AFNetworking являтся де-факто стандартом библиотеки для работы с сетевыми данными. Хоть ее «универсальность» и тянет за собой кучу ненужной функциональности, свою мы решили не писать.


Библиотека привносиит функционально-реактивные краски в мир iOS (

). В данный момент активно используется в приложениях–ассистентах. На начальном этапе она показалась мне удобным способом описывать запросы как отдельные юниты работы API (зачем это понадобилось, будет рассказано в секции про цепочки запросов ниже).

Еще одна библиотека от iOS команды Github, которая позволяет значительно упростить слой модели данных, а именно парсинг ответов web-сервисов (пример в README весьма показателен). В качестве бонуса все объекты автоматически получают поддержку

История развития публичных api

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

Как это работает?

Авторизация работает с использованием ассиметричного шифрования (RSA-2048) пакет имеет свою кастомную структуру.

После успешной авторизации клиент переключается на симметричное шифрование по заранее согласованному сеансовому ключу.

Качество кода

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

Конкурс

Сейчас мы готовы перейти к следующему этапу поддержки проекта и взаимодействия с игровым сообществом — открытому конкурсу для разработчиков.

Wargaming Developers Contest будет направлен на поддержку разработчиков игровых

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

Для всего этого в рамках Wargaming Developers Contest мы приготовили отличный призовой фонд; кроме того, уникальные и оригинальные проекты мы обязательно представим нашей многомиллионной аудитории.

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

Максимальное покрытие кода тестами

Я уже немного писал на Хабре про

. Частично эти наработки были использованы при тестировании библиотеки. 


Покрыть тестами код библиотеки оказалось делом весьма тривиальным (98%). 

Большинство тестовых сценариев можно условно поделить на два вида:

Немного статистики

Нужно сказать, что с введением Public API как минимум одну из насущных проблем мы решили — нехарактерная нагрузка на наши ресурсы, которая периодически сильно нам портила жизнь, значительно снизилась. Вернее, перетекла в API, где ее намного проще контролировать и обеспечивать надежность работы как системы в целом, так и сервиса для сторонних разработчиков.

Вот немного актуальных цифр для RU-реалма:

Необходимость ручного допиливания напильником

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

Возмьем для примера метод Техника (encyclopedia/vehicles).

Для уменьшения объема ответа отфильтруем ответ по технике одного уровня и одной нации. Вызов следующего кода вызовет ошибку:

var client = new WGClient.Client(); 
var response = await client.SendRequestDictionary<ResponseWotEncyclopediaVehicles>(new RequestWotEncyclopediaVehicles() 
{ 
    ApplicationId = "demo", 
    Tier = "8", 
    Nation = "ussr" 
}); 

Выкинув исключение:

Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type ‘WGClient.WorldOfTanks.WotEncyclopediaVehiclesCrew’ because the type requires a JSON object (e.g. {«name»:«value»}) to deserialize correctly.

To fix this error either change the JSON to a JSON object (e.g. {«name»:«value»}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.

Отсюда следует, что не удалось десериализовать подтип Crew, и если мы построим запрос в API Explorer, то увидим, что Crew возвращается в виде массива:

При этом поле Engines корректно распозналось благодаря тому, что для него был указан тип поля «list of integers» в отличие от подтипа Crew, для которого нет никакой информации.

Исправить ошибку можно, сделав поле Crew массивом, заменив поле:

///<summary> 
///Экипаж 
///</summary> 
[JsonProperty("crew")] 
public WotEncyclopediaVehiclesCrew Crew { get; set; } 

на массив:

public WotEncyclopediaVehiclesCrew[] Crew { get; set; } 

Аналогичную ошибку получаем для default_profile.ammo, соответственно, там тоже необходимо исправить сделав массив.

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

Неоптимальное api клиента


В конченом итоге остановился на достаточно многословном и не очень удобном тяжеловесном варианте:

SendRequest<TResponse>(TRequest request) 

Есть масса способов сделать решение проще. Но максимально простой вариант API можно получить, если компания WG не полениться доделать описание своего собственного API (а именно как нибудь регламентировать что именно можно ожидать от запроса в качестве ответа: словарь, массив или единичный объект в корневом и дочерних узлах ответа).

Нормализация признаков

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

Пакеты (ассиметричное шифрование)

Открываем WireShark — ставим фильтр на UDP протокол и делаем попытку авторизации.

Шифрование (Кто бы мог подумать что мы его найдем)Клиент отправляет на сервер зашифрованный пакет с фиксированным размером (273 байт) и получает незашифрованный пакет от сервера. Выполнив несколько попыток авторизоваться, я решил сравнить пакеты:

Помеченные байты не меняются (только незначимые 1-2 байта) Структура пакета получается следующей:

HEADER => [0x01, 0x00, 0x00, 0x04, 0x01, 0x31, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x02]
BODY => [? ? ? ? ?]
FOOTER => [0x02, 0x00]

Размер тела пакета равна 256 байт * 8 = 2048 бит (вспоминаем о публичном ключе RSA-2048)

Перенаправление пользователя на сайт wg


В примере, скрипт сам делает перенаправление пользователя после генерации ссылки.

Плохой интернет

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

Поддержка «частичных» ответов

Если мы посмотрим в докуменатцию по любому из запросов API (например,

), мы увидим там поле
fieldsСписок полей ответа. Поля разделяются запятыми. Вложенные поля разделяются точками. Если параметр не указан, возвращаются все поля.
То есть, получая объект
Player
, мы можем получить как полный JSON-граф, так и частичный. Первая и очевидная часть решения заключается в том, что если передаются поля в ключе
fields
, в ответе из библиотеки мы получаем нетипизированные
NSDictionary
. 

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

Для маппинга
JSON -> NSObject
у нас используется
Mantle
, и имплементация запроса к API в общем случае выглядит так (про
RACSignal
и публичный API в целом я расскажу чуть позже):

- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit {
	NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
	parameters[@"search"] = query;
        if (limit) parameters[@"limit"] = @(limit);
	
	return [self getPath:@"account/list"
			  parameters:parameters
			 resultClass:WOTSearchPlayer.class];
}

Как видим, у нас есть уже есть параметр
resultClass
, так почему же не вынести его в сигнатуру метода? Получаем:

 - (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit resultClass:(Class)resultClass

Да, у нас раздувается публичный API, но зато теперь мы имеем способ типизировать объекты на стороне библиотеки.

Покрытие тестами

1-2 раза в месяц выходит новая версия API. Соответственно перегенерировав весь ответ мы автоматически потеряем все правки напильником, которые у нас уже были сделаны.

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

Предпосылки создания wargaming public api

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

Причем, такая картина не устраивает не только сторонних разработчиков. Большое количество запросов, сканирующих информацию с сайта, вполне могут создавать приличную нагрузку на сервера, а то и полностью «уложить» сайт. Особенно если учесть, что приложение, как правило, оптимизируется под поведение пользователя, а не краулера.

Собственно, в этом плане Wargaming ничем не отличается — как только «танки» стали популярными, сразу же возник спрос на информацию по профилям игроков, кланам, статистике, рейтингам, глобальной карте, энциклопедиям и т.д. Разумеется, начался парсинг сайтов в попытках вытащить эти данные. А как только вышел World of Tanks Assistant, на его API немедленно набросились страждущие.

Сделать вывод из всего этого было несложно: раз информация сторонним разработчикам нужна, они все равно ее добудут. Но лучше в таком случае предоставить им публичный API. Управлять им намного проще, он предоставляет более стабильный, удобный и надежный инструмент разработчикам, а нам как минимум позволяет экономить трафик и снижать нагрузку за счет более оптимизированных алгоритмов под такие сценарии использования.

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

Приложения

Всего активных на текущий момент приложений — более 200. Цифры не самые большие, но среди приложений много достойных, а на некоторые, даже по очень грубой оценке, потрачено порядочно времени и усилий. Например:

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

Кстати, Wargaming вовсе не против того, чтобы хорошие приложения монетизировались и приносили доход своим разработчикам.

Пример вызова такой функции (caller)

Все что нам нужно это сделать сплайсинг данной функции.

Сплайсинг (от англ. splice) — метод перехвата API функций путём изменения кода целевой функции. Вместо них вставляется переход на функцию, которую определяет программист.

Для начала нам нужно вызвать оригинальную функцию дешифровки.После этого прочитать [ptr* dest]

Я решил написать свою Dll на C чтобы сделать трамплин функцииНе забываем о соблюдении соглашении при вызове (__cdecl / __fastcall / __thiscall)

  1. Получаем адрес функции через GetModuleBaseAddress RVA

  2. Делаем сплайсинг функции

Пример использования:

Исходники нижеприведенного примера можно забрать здесь.

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

Для примера потратил 15 минут и создал тестовое Xamarin Forms приложение без дизайна, где набросал, в первой форме поиск по нику, а во второй форме информацию о выбранном нике среди найденных.

В созданный проект добавил cs файл, куда скопировал код , сгенерированный нашей утилитой.

Следующий шаг — добавление библиотеки Json.net (поиск в nuget библиотеки Newtonsoft.Json или командой Install-Package Newtonsoft.Json c nuget консоли).

Код поиска получается достаточно простым:

var client = new WGClient.Client(); 
var accounts = await client.SendRequestArray<ResponseWgnAccountList>(new RequestWotAccountList() 
{ 
    ApplicationId = "demo", 
    Search = SearchNickname 
}); 
GamerAccounts = accounts; 

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

Обратите внимание, ApplicationId: «demo» можно использовать только для тестирования API. Для релиза необходимо создать свой ApplicationId в личном кабинете

Теперь осталось отобразить список найденных Nickname:

К сожалению, у меня уже нет своего аккаунта, спасибо Шериеву Амиру (моему брату) за предоставленный игровой ник для растерзания в примерах.

По тапу из списка найденных открываем вторую форму, передав выбранный AccountId:

var item = e.SelectedItem as ResponseWgnAccountList; 
Navigation.PushAsync(new DetailsPageView(item.AccountId)); 


На второй странице тоже создается запрос к другому методу для получения более подробной информации:

var client=new Client(); 
var response=await client.SendRequestDictionary<ResponseWotAccountInfo>(new RequestWotAccountInfo() 
{ 
    ApplicationId = "demo", 
    AccountId = accountId 
}); 

wotapi · PyPI

Таким образом, с помощью нашего сгенерированного клиента у нас есть возможность сэкономить огромное количество времени на автоматизацию рутины формирования запроса и ответа к WG API и сосредоточить время, силы и внимание на само приложение.

Принципы взаимодействия

В нашей инфраструктуре есть два основных потока данных: вызов удаленных методов API и подписка на события другой подсистемы (игрового сервера, других компонентов).

Проверка данных, которые получили после возврата пользователя


Если мы планируем делать авторизацию, то должны быть уверенны, что полученные данные правдивы и переданы именно с сайта WG, а не прописаны вручную.

Работа с признаками

Для анализа данных я использовал питоновскую библиотеку pandas. Загрузим все данные в pandas.DataFrame, получили 450 строк и 40 колонок. Список всех признаков:

Все фичи должны быть интуитивно понятны, кроме ap_damage, apcr_damage, he_damge, hc_damage и такие же с _penetration. Это урон и бронепробитие разными типами снарядов. API возвращает информацию об орудии в виде массива объектов, которые содержат данные о уроне и бронепробитие для конкретного типа снарядов. Их есть 4 типа:

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

Разработка

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

и перерабатывает в готовые модели запросов и ответов

К большому сожалению документация WG API не полная и в некоторых моментах понять какой именно ответ вернется можно только из уже полученного ответа с JSON.

Позже хаброжитель thunderspb подсказал значительно более простой способ получения схемы данных, из за чего я отложил публикацию статьи (почти на два месяца из за острой нехватки времени). К сожалению и эта схема данных обладает ровно теми же недостатками — нет возможности понять формат ответа для составных типов данных.

Реализация:

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

Как было сказано выше, узнать что же будет на выходе можно будет только отправив запрос и получив ответ в виде JSON, где и увидим в живых данных, как именно возвращаются данные (хорошо что в документации есть ссылка на API Explorer в каждом методе, который позволяет очень легко формировать запрос и видеть ответ прямо в браузере).

В конечном итоге, у нас получилось очень простое приложение, где по очереди открываются страницы с WG API и распарсиваются. После этого генерируется и выводится C# код клиента в отдельном окне:

Резюме

Сам факт того что Wargaming старается быть открытым к разработчикам и старается не просто открывать API, но еще и детально описывает значения полей очень похвальна. Несмотря на существенные недостатки в описании и в самой структуре ответа (а именно то что нельзя понять что будет возвращен, одиночный ответ, массив или словарь для сложных типов), это одно из лучших документаций API для подобных сервисов.

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

Создание и отбор признаков

На основе исходных данных можно получить более информативные признаки:

df['power'] = df.engine_power / (df.weight / 1000) # в лошадях на тонну 
df['max_damage'] = df[['ap_damage', 'apcr_damage', 'he_damage', 'hc_damage']].max(axis=1)
df['max_penetration'] = df[['ap_penetration', 'apcr_penetration', 'he_penetration', 'hc_penetraion']].max(axis=1)
df['dpm'] = df['max_damage'] * df['gun_fire_rate'] # урон в минуту
def get_armor(y):
    # если есть башня, то берём среднее значение лобовой брони башни и корпуса
    # если нет, то просто берём лобовую броню корпуса
    if y[1]:
        return np.mean(y[:2])
    else:
        return y[0]
df['armor'] = df[['armor_hull_front', 'armor_turrer_sides']].apply(get_armor, axis=1) 

Методом проб и ошибок (random forest) я отобрал самые значимые признаки (но далее мы также рассмотрим еще два интересных признака):

Для тех, кто не играл в WOT, здесь отображены: уровень танка (от 1 до 10), премиумный танк или нет, количество очков прочности, мощность (лошадей/тонну), скорострельность(выстрелов/минуту), скорость сведения орудия(сек), разброс орудия(метры), скорость вперёд(км/ч), максимальный урон(хп), максимальное бронепробитие(мм), урон в минуту(хп/мин), броня(мм).

Теория

Для аутентификации пользователей, на данный момент, у WG API, есть три метода:

— метод используется для аутентификации пользователя — получения access_token.

— с помощью этого метода можно продлить access_token без участия пользователя

— метод для уничтожения access_token

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

Для получения access_token, как уже сказано выше, используется метод auth/login, который отправляет информацию о статусе авторизации по адресу указанному в параметре redirect_uri

Один из разработчиков WG сообщил:

Принято решение расширить возможности метода auth/login и будет введено разделение (при успешно введенных сведениях):
1) На один URL будет сделана переадресация пользователя;
2) На второй будет отправлены сведения по авторизации (методом POST или GET).

Вернемся к теории.

Тест модели

describe(@"WOTRatingType", ^{
	it(@"should parse json to model object", ^{
		NSDictionary *json = @{
							   @"threshold": @5,
							   @"type": @"1",
							   @"rank_fields": @[
									   @"xp_amount_rank",
									   @"xp_max_rank",
									   @"battles_count_rank",
									   @"spotted_count_rank",
									   @"damage_dealt_rank",
									   ]
							   };
		
		NSError *error;
		WOTRatingType *ratingType = [MTLJSONAdapter modelOfClass:WOTRatingType.class
                                              fromJSONDictionary:json
                                                           error:&error];
		
		[[error should] beNil];
		[[ratingType should] beKindOfClass:WOTRatingType.class];
		
		[[json[@"threshold"] should] equal:theValue(ratingType.threshold)]; *
		[[json[@"type"] should] equal:ratingType.type];
		[[ratingType.rankFields shouldNot] beNil];
		[[ratingType.rankFields should] beKindOfClass:NSArray.class];
		[[ratingType.rankFields should] haveCountOf:5];
		
		[[@"maximumXP" should] equal:ratingType.rankFields[1]];
	});
});

* — в тестах используется Йода-нотация из-за неприятного бага в Kiwi, который не указывает строку с ошибкой, если значение переменной равно nil.

Воркфлоу при добавлении нового запроса в API состоит из двух шагов:

  1. написать маппинги и запрос;
  2. Написать два теста.

Требования

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

Удобный способ для обработки цепочек запросов

Часто при работе с API мы сталкивались с вариантом использования, когда существовал как минимум один вложенный запрос и результатом операции являлась комбинация ответов двух запросов: например, получаем список пользователей, а затем дополнительно вытягиваем «неполный» список техники для каждого. (Иногда таких запросов бывает три). 

Все представляют, как выглядит использование трех вложенных блоков (псевдокод на основе
AFJSONRequestOperation

    op1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil
                                                          success:^(id JSON) {
                                                              
        op2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil
                                                              success:^(id JSON) {
            
            op3 = [AFJSONRequestOperation JSONRequestOperationWithRequest:nil
                                                                  success:^(id JSON) {
                // Combine all the responses and return
            } failure:^(NSError *error) {
                // Handle error
            }];
        } failure:^(NSError *error) {
            // Handle error
        }];
    } failure:^(NSError *error) {
        // Handle error
    }];

Мало того, что растет уровень вложенности, так еще и обработать ошибку в одном месте будет сложно. В тот момент я как раз играл с
ReactiveCocoa
и подумал, что неплохо бы ее попробовать в продакшене.


Публичное API библиотеки выглядит следующим образом:

- (RACSignal *)searchPlayers:(NSString *)query limit:(NSUInteger)limit;
- (RACSignal *)fetchPlayers:(NSArray *)playerIDs;

RACSignal
— юнит работы, который лениво выполняется только в том случае, когда его об этом просят. Так как юниты — это просто представление некой работы в будущем, их можно всячески комбинировать. Ниже приведен абстрактный пример склейки трех запросов и получения ответа/обработки ошибки:

    RACSignal *fetchPlayersAndRatings =
        [[[[API searchPlayers:@"" limit:0]
          
          flattenMap:^RACStream *(id players) {
            // skipping data processing
            return [API fetchPlayers:@[]];
        }]
         
         flattenMap:^RACStream *(id fullPlayers) {
            // Skipping data processing
            return [API statsSliceForPlayer:nil hoursAgo:@[] fields:nil];
        }]
         
         flattenMap:^RACStream *(id value) {
            // Compose all responses here
            id composedResponse;
            return [RACSignal return:composedResponse];
        }];
    
    [fetchPlayersAndRatings subscribeNext:^(id x) {
        // Fully composed response
    } error:^(NSError *error) {
        // All erros go to one place
    }];

Использовать
ReactiveCocoa
или нет — само по себе является большим холиваром, так что оставим его за скобками. Если бы мы не использовали библиотеку в приложениях, вполне могли бы обойтись более легковесными библиотеками для Promises и Futures.

Удобный способ интеграции в приложения

Библиотека на данный момент состоит из трех частей:

Логично предположить, что код библиотеки хотелось бы хранить в одном месте, а вот встраивать в приложения — частями. Естественно, с самого начала (когда еще не было самолетов) мы поддерживали интеграцию через приватный репозиторий

, так что разделение не составило большого труда. 
Мы использовали фичу под названием
subspecs
, которая позволяет разделить код библиотеки на три части:

  # Subspecs
  s.subspec 'Core' do |cs|
    cs.source_files = 'WGNAPIClient/Core/**/*.{h,m}'
  end

  s.subspec 'WOT' do |cs|
    cs.dependency 'WGNAPIClient/Core'
    cs.source_files = 'WGNAPIClient/WOT/**/*.{h,m}'
  end

  s.subspec 'WOWP' do |cs|
    cs.dependency 'WGNAPIClient/Core'
    cs.source_files = 'WGNAPIClient/WOWP/**/*.{h,m}'
  end

Теперь можно использовать «танковую» и «самолетную» части отдельно, развивая библиотеку в рамках одного проекта:

pod 'WGNAPIClient/WOT'
pod 'WGNAPIClient/WOWP'

Шифрование (симметричное шифрование)

После успешной авторизации, от сервера прилетает зашифрованный пакет и тут включается симметричное шифрование.

Немного проанализировав функции я нашел функцию которая отвечает за дешифровку пакета

Сигнатура функции выглядит приблизительно вот так

Заключение

Мы посмотрели, как работать с WG API. Узнали, как винрейт зависит от нации — на данный момент на чехах он самый нестабильный, от класса техники — на средних танках самый большой, а на арте самый маленький. Также увидели прямолинейную зависимость от уровня.

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

Upd: saw_tooth навёл на мысль построить график по винрейту от уровня техники и типа отдельно (кликабельно):
wotapi · PyPI
P.S.: Если вы тоже хотите поработать с этим датасетом, но не хотите загружать данные через API то пишите мне.

Про WoT:  Бан в World of Tanks за использование читов - март 2020 года
Оцените статью
TankMod's
Добавить комментарий