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

После прочтения постов о 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.  


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

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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    +2
    Интересная статья, спасибо. С нетерпением буду ждать продолжения.
    С Новым Годом!
      0
      Действительно интересный пост.
      Но бота в игре забанят (… русские понабежали и появились боты, киборги, дед мороз и снегурка )))).

      Актуален вопрос автоторговли по условиям пользователя.
        +1
        Торговаля в игре самое неудобное, «торговый клиент» был бы удобен когда можно самому как угодно всё сделать =)
          0
          Я говорил о проведении торговли на условиях.
          Условия задаются игроком.
          И условий должно быть много.
          Например если есть в продаже 200 мрамора за 20 золота — то покупать автоматом.
          А не тыкать каждые 10 минут «Aktualisieren».
            –1
            Это да, просто иногда надо купить что-то один-два раза, а в игре нету даже примитивной сортировки, что осложняет поиск наиболее выгодных предложений.
              +1
              Сортировка есть. Нужно тыцнуть мышкой на заголовке, например, «Koshten» после того, как покажется список товаров.
                0
                Ага, сортировка. По одному полю, самым примитивным образом:
                1
                11
                16
                2
                37
                5
                9
                Удобнее было бы выбирать что нужно\что продаем и в списке от наиболее выгодных к менее.
            • UFO just landed and posted this here
            0
            Забанить достаточно сложно, в принципе.
            Авто-торговля будет через скрипты.
            –1
            Спасибо! Однозначно нужно продолжать! И интересно и полезно!
              –1
              Видимо вы раньше работали с flash/flex приложениями. На счёт продолжать, это однозначно!
              Было бы еще полезно выкладывать исходный результат ввиде кода.
                0
                Да, писал сервера под флеш-фронтенды :)
                Скоро открою реп, только чуть-чуть после НГ отойду
                  0
                  Нет. Не нужен исходный код. Отличная статья, отлично написано. Человек с головой всё сам напишет. Человек без головы по готовым исходникам только засрёт игру говноботами.
                    0
                    А вы учитываете, что будет продолжение циклов статей и то что некотрые людям легче понять, как работает программа анализируя код
                      0
                      Учитываю.
                      А ещё людям легче взять что-нибудь готовое и не вникать в то, как оно работает. Вы это учитываете? :)

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

                      Повторю ещё раз: исходники публиковать не нужно.
                –1
                Надо продолжать!
                  0
                  NetConnection.Call.BadVersion
                  Конструктор для типа game.asd.asd.ItemId не найден.

                  Описываю

                  namespace game.asd.asd
                  {
                  [AmfObjectName(«game.asd.asd.ItemId»)]
                  public class EstateId
                  {
                  [AmfObjectName(«z»)]
                  public int z { get; set; }

                  [AmfObjectName(«x»)]
                  public int x { get; set; }

                  [AmfObjectName(«y»)]
                  public int y { get; set; }

                  public ItemId(int z1, int x1, int y1)
                  {
                  z = z1;
                  x = x1;
                  y = y1;
                  }
                  }
                  }

                  но что-то ему другое нужно…

                  Only users with full accounts can post comments. Log in, please.