Как стать автором
Обновить

Amf и C# на примере бота для Settlers online

Время на прочтение 7 мин
Количество просмотров 11K
После прочтения постов о Settlers и конкретно Пишем утилиту для Settlers Online, возникло желание несколько оптимизировать описанный там процесс, т.к. подход меня несколько смутил, и, заодно, рассмотреть аспект amf формата в C#. Предположим, что выбрана задача написать бота для этой игры.

Для начала нам необходимо разобраться с этапами логина.


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

1. Charles — отладочный прокси, поддерживающий формат amf сообщейний (триал раздражает долгим наг-скрином, раз в пол-часа выключается).
2. Fiddler — тоже прокси, намного лучше справляющийся с https траффиком (бесплатный).
3. Что-то для просмотра самой флешки, тут на выбор:
 — AS3 Sorcerer — быстрый, легкий, хорошо справляется с протекторами флеша (есть триал, иногда отображающий наг-скрин с веселыми картинками).
 — связка SWF Decrypt (бесплатный) + любой декомпилятор флеша, например http://www.sothink.com/ (триальная версия имеет урезанный функционал, но нам хватит).
4. FluorineFx — open source framework, поддерживающий amf0 и amf3 форматы.

Вооружившись всем необходимым, можно приступать к разбору этапов логина игры:

1. Непосредственно логин, используя https к https://www.diesiedleronline.de/de/api/user/login
2. Получение ключа сессии и пароля на чат-сервере, используя «Big Brother» сервис http://bb.diesiedleronline.de/
3. Первые пакеты уже к основному гейтвею игры.

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


Расписывать как делаются элементарные вещи, вроде http-запросов я не буду, предполагая, что Вам это известно. Единственное замечение, что нам нужно обработать момент с верификацией сертификата, для этого достаточно добавить такой код:
  1.  
  2. System.Net.ServicePointManager.ServerCertificateValidationCallback
  3.                 = delegate(object sender,
  4.                            X509Certificate certificate,
  5.                            X509Chain chain,
  6.                            SslPolicyErrors sslPolicyErrors)
  7.             {
  8.                 return true;
  9.             };
  10.  


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

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

FluorineFx



Игра написана с использованием Flex'a, поэтому использует amf3 формат сообщений. Для коннекта к серверу нам нужен адрес гейтвея (получается у ББ), имя функции, которая дергается на сервере, непосредственно параметры.
Итак, запускаем игру, открываем Charles и смотрим формат уходящих пакетов 

Выходит, что на сервер уходит объект dServerCall, содержащий в dataObject'e дополнительные параметры и имеющий целочисленный type.
ОК, все просто, быстро набрасываем класс и запинаемся на весьма элементарной вещи…
FluorineFx — замечательная библиотека устраивает нас всем, кроме элементарного — маппинга (сопоставления) имен классов и их полей. Прийдется ее немного пропатчить, для этого заведем собственный атрибут
  1.  
  2. [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
  3. public class AmfObjectName : System.Attribute
  4. {
  5.     public const string DefaultPrefix = "defaultGame.Communication.VO.";
  6.    
  7.     public string Name { get; set; }
  8.    
  9.     public AmfObjectName(string name)
  10.     {
  11.         Name = name;
  12.     }
  13. }
  14.  

С помощью него мы будем сопоставлять имена объектов и их свойств в нашем боте и на сервере.
Продебажив библиотеку можно понять, что нам нужно вставить проверку наличия атрибута в IO.AMFWriter'e в методах WriteAMF3Object (помните, что мы используем флекс, поэтому только amf3 формат) и GetMember. Получается достаточно просто 
  1.  
  2. if (IsClassAttributed(type))
  3. {
  4.     propertyInfo = FindProperty(type, member.Name);
  5. }
  6.  

где
  1.  
  2. public static bool IsClassAttributed(Type type)
  3. {
  4.     var res = type.GetCustomAttributes(typeof(AmfObjectName), false);
  5.     return null != res && res.Length > 0;
  6. }
  7.  
  8. public static PropertyInfo FindProperty(Type type, string amfObjName)
  9. {
  10.     foreach (var prop in type.GetProperties())
  11.     {
  12.         var attrs = prop.GetCustomAttributes(typeof(AmfObjectName), false);
  13.         if (null == attrs || 0 == attrs.Length || ((AmfObjectName)attrs[0]).Name != amfObjName)
  14.         {
  15.             continue;
  16.         }
  17.        
  18.         return prop;
  19.     }
  20.    
  21.     return null;
  22. }
  23.  


Аналогичным образом правим AMFReader. Конечно, можно и, собственно, нужно настроить элементарное кеширование найденных свойств, но описывать этот процесс я не буду, ибо там все просто.
Кроме того, для того, чтобы при десериализации данных наши классы были видимы для создания инстансов, необходимо «зарегестрировать» их в кеше Fluorine. Для этого ищем класс ObjectFactory и добавляем метод

  1.  
  2. public static void AddToLocate(Type type, string mapName)
  3. {
  4.     _typeCache.Add(mapName, type);
  5. }
  6.  


и при старте нашего приложения регистрируем все наши классы

  1.  
  2. foreach(var type in Assembly.GetExecutingAssembly().GetTypes())
  3. {
  4.     var attrs = type.GetCustomAttributes(typeof(AmfObjectName), false);
  5.     if (null == attrs || 0 == attrs.Length)
  6.     {
  7.         continue;
  8.     }
  9.    
  10.     ObjectFactory.AddToLocate(type, AmfObjectName.DefaultPrefix + ((AmfObjectName)attrs[0]).Name);
  11. }
  12.  


Кстати, AmfObjectName.DefaultPrefix заведен просто для того, чтобы во всех классах не писать одно и тоже в атрибутах:)

Ну и теперь пришла пора описать наш класс:

  1.  
  2. namespace SettlersControl.Objects
  3. {
  4.     [AmfObjectName("dServerCall")]
  5.     public class SettlerRequest
  6.     {
  7.         public static int PlayerZoneId = 0;
  8.        
  9.         [AmfObjectName("type")]
  10.         public int Type { get; set; }
  11.         [AmfObjectName("zoneID")]
  12.         public int ZoneId { get; set; }
  13.         [AmfObjectName("dataObject")]
  14.         public object DataObject { get; set; }
  15.        
  16.         public SettlerRequest(SettlerMessages message, object dataObject)
  17.         {
  18.             Type = (int)message;
  19.             ZoneID = PlayerZoneId;
  20.             DataObject = dataObject;
  21.         }
  22.     }
  23. }
  24.  


Все готово для первой отправки данных и разбора что же собственно заставляет Charles не справляться с парсингом. Открываем пример из библиотеки, делаем, запускаем и… получаем в ответ только исключение. Не зря, видимо, Чарли падал. ОК, но зато у нас есть имя виновника: dUniqueID. Судя по исключению не удается обработать IExternalizable элемент. Идем в Гугл и находим, что это определенный формат, позволяющий пользователю самому сериализовывать объект. Вот и первая проблема. Не зная формата объекта и очередности записи его полей ничего нам не светит. Поэтому нам и пригодится возможность подсмотреть что же там сделали разработчики. Открываем Sorcerer, натравливаем на флешку, ищем нужный файл и видим нужные нам данные

Отлично, осталось реализовать это, воспользовавшись интерфейсом, заботливо предоставленным разработчиками Fluorine:
  1.  
  2. [AmfObjectName("dUniqueID")]
  3. public class SettlerUniqueId : IExternalizable
  4. {
  5.     [AmfObjectName("uniqueID1")]
  6.     public int UniqueID1 {get; set; }
  7.     [AmfObjectName("uniqueID2")]
  8.     public int UniqueID2 {get; set; }
  9.    
  10.     public void ReadExternal(IDataInput input)
  11.     {
  12.         UniqueID1 = input.ReadInt();
  13.         UniqueID2 = input.ReadInt();
  14.     }
  15.    
  16.     public void WriteExternal(IDataOutput output)
  17.     {
  18.         output.WriteInt(UniqueID1);
  19.         output.WriteInt(UniqueID2);
  20.     }
  21. }
  22.  


Осталось по аналогии оформить необходимые нам классы, побороть ошибку и, наконец-то, первые данные получены:

Понятно, что это самое начало, но основные проблемы всегда бывают на старте:)
Если будут желающие, то буду продолжать писать бота и выкладывать результаты.
Теги:
Хабы:
+26
Комментарии 17
Комментарии Комментарии 17

Публикации

Истории

Работа

.NET разработчик
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн