вторник, 25 декабря 2012 г.

Сделай паузу

Некоторое время назад, мной был разработан Framework, предназначенный для создания несложных 2D-игр, с использованием инструментальной платформы Marmalade, который я описал в цикле статей этого блога. Разумеется, все что я описывал в статьях было предельно упрощено для того, чтобы не увязнуть в деталях реализации, по ходу изложения. Созданному Framework-у не хватает многих возможностей без которых немыслимо его применение в реальных проектах. Сегодня я хочу рассказать о добавлении одной из них.

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

Начнем с интерфейсов. В метод IObject.update добавим флаг isPaused:

class IObject {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused) = 0;
};

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

class AbstractSpriteOwner: public ISpriteOwner {
    public:
                   ...
        virtual void update(uint64 timestamp, bool isPaused);
        virtual void correctPauseDelta(uint64 pauseDelta);
};


Реализация метода correctPauseDelta тривиальна. Мы просто передаем продолжительность паузы в миллисекундах всем вложенным спрайтам:

void AbstractSpriteOwner::correctPauseDelta(uint64 pauseDelta) {
    for (ZIter p = zOrder.begin(); p != zOrder.end(); ++p) {
        p->second->addPauseDelta(pauseDelta);
    }
}


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

void AnimatedSprite::addPauseDelta(uint64 pauseDelta) {
    for (CIter p = currentMessages.begin(); p != currentMessages.end(); ++p) {
        p->timestamp += pauseDelta;
    }
}

void AnimatedSprite::update(uint64 timestamp, bool isPaused) {
    if (isPaused && isPausable()) return;
         ...
}


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

class AbstractScreenObject: public IScreenObject {
    public:
                   ...
        virtual void addPauseDelta(uint64 pauseDelta) {}
        virtual bool isPausable() const {return true;}
};


Чтобы внести изменения в CompositeSprite, необходимо помнить, что он, с одной стороны, является контейнером спрайтов, в то время как с другой, сам является наследником AnimatedSprite и может обрабатывать правила анимации:

void CompositeSprite::addPauseDelta(uint64 pauseDelta) {
    AbstractSpriteOwner::correctPauseDelta(pauseDelta);
    AnimatedSprite::addPauseDelta(pauseDelta);
}

void CompositeSprite::update(uint64 timestamp, bool isPaused) {
    AnimatedSprite::update(timestamp, isPaused);
    AbstractSpriteOwner::update(timestamp, isPaused);
}


Основную работу по приостановке и возобновлению анимации возьмет на себя реализация Scene.

Scene.h:

class Scene: public AbstractSpriteOwner {
    protected:
                  ...
        bool IsPaused;
        uint64 pauseTimestamp;
    public:
                  ...
        virtual void update(uint64 timestamp, bool isPaused);
        bool isPaused() const {return IsPaused;}
        void suspend();
        void resume();
};

Scene.cpp:

void Scene::update(uint64 timestamp, bool) {
    if (IsPaused && (pauseTimestamp == 0)) {
        pauseTimestamp = pauseTimestamp;
    }
    if (!IsPaused && (pauseTimestamp != 0)) {
        uint64 pauseDelta = timestamp - pauseTimestamp;
        if (pauseDelta > 0) {
            correctPauseDelta(pauseDelta);
        }
        pauseTimestamp = 0;
    }
         ...
    AbstractSpriteOwner::update(timestamp, IsPaused);
}

bool Scene::sendMessage(int id, int x, int y) {
    if (AbstractSpriteOwner::sendMessage(id, x, y)) {
        return true;
    }
    if (IsPaused) return false;
    if (background != NULL) {
        return background->sendMessage(id, x, y);
    }
    return false;
}

void Scene::suspend() {
    desktop.suspend();
    IsPaused = true;
}

void Scene::resume() {
    desktop.resume();
    IsPaused = false;
}


Осталось добавить код приостановки фоновой музыки в класс Desktop.


Desktop.h:

class Desktop {
    private:
                   ...
        bool isMusicStarted;
    public:
        Desktop(): touches(), names(), currentScene(NULL), isMusicStarted(false) {}
                  ...
        void startMusic(const char* res);
        void stopMusic();
        void suspend();
        void resume();
};

Desktop.cpp:

void Desktop::startMusic(const char* res) {
    if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
        s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
        s3eAudioPlay(res, 0);
    isMusicStarted = true;
}

void Desktop::stopMusic() {
    isMusicStarted = false;
    s3eAudioStop();
}

void Desktop::suspend() {
    if (isMusicStarted) {
        s3eAudioPause();
    }
}

void Desktop::resume() {
    if (isMusicStarted) {
        s3eAudioResume();
    }
}


Теперь мы умеем останавливать время в нашем приложении :)

Проект на GitHub обновлен.

понедельник, 17 декабря 2012 г.

Скриптинг для бюджетной активации (часть 1)

Некоторое время назад, мне довелось поучаствовать в крупном международном проекте в составе команды активации. Суть проекта сводилась к автоматизации выполнения ряда команд на оборудовании Cisco. Разработка активационных скриптов велась на JavaScript. Главная мысль, которую я вынес из этого проекта, заключалась в том, что разработка и отладка активационных скриптов на JavaScript крайне трудоемкое занятие. Интенсивная разработка велась в течение 1 года, а только наша группа активации включала в себя около 10 разработчиков (тестеров требовалось не меньше).

В июне этого года, уже местное руководство, осчастливило меня новым активационным проектом, в котором маршрутизаторы Cisco сменились на АТС Alcatel S12 и M200. Вторым отличием этого проекта было то, что всю активационную часть предстояло разработать мне одному, с нуля, в течение полугода.

Разумеется, у меня и мысли не возникло использовать для активационных скриптов JavaScript или что-то подобное. Мне был необходим бюджетный вариант скриптинга. После некоторых размышлений, я решил хранить скрипты активации в БД Oracle, используя для хранения следующую структуру:



Из этой схемы видно, что части скрипта (AE_SCRIPT_PART), представляющие собой скрипт (AE_SCRIPT) или команду (AE_COMMAND), связываются в иерархическую структуру, посредством таблицы AE_SUBSCRIPT (первоначально AE_SCRIPT_PART просто содержала поле PARENT, ссылающееся на эту-же таблицу, но такое решение препятствовало повторному использованию фрагментов скриптов). Таблица AE_SETTING служит для привязки к скриптам и командам значений ряда настроек, определенных AE_SETTING_TYPE, таких как условие выполнения (if_condition) или переменная цикла (for_each).

Первоначально была предусмотрена возможность создания шаблонов команд (AE_COMMAND_TEMPLATE), связанных с набором заранее определенных значений настроек, но эта возможность оказалась избыточной и, в настоящее время, почти не используется. Фактически, эта схема данных позволяет хранить AST.

Чтобы не заблудиться во всем этом безобразии, параллельно наполнению скрипта в БД, велся Excel-файл следующего вида:


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

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


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

Потом я подумал еще немного и решил использовать PL/SQL, а не Perl. Действительно, одним вызовом хранимой процедуры, я мог сформировать текст скрипта в CLOB, и это было ничуть не хуже чем скрипт в текстовом файле. Вот что получилось в результате:


create or replace package body ae_scripting as

    g_if_setting          constant varchar2(100) default 'if_condition';
    g_foreach_setting     constant varchar2(100) default 'foreach_var';
    g_target_setting      constant varchar2(100) default 'target_platform';
   
    g_if_statement        constant varchar2(100) default 'if';
    g_foreach_statement   constant varchar2(100) default 'foreach';
   
    procedure extract(p_id in number) as
    cursor c_cmd is
    select d.id, d.nm, d.lv, d.type_id, p.name platform, t0.value target, 
           f.direction_id direction
    from   ( select level lv, sys_connect_by_path(to_char(c.nm, '00000'), '/') pt,
                    sys_connect_by_path(c.id, '.') nm, c.id id, c.type_id
       from  ( select a.id id, a.type_id type_id, b.parent_id parent_id, 
                      b.order_num nm
               from   ae_script_part a, ae_subscript b
               where  b.child_id(+) = a.id ) c
       start   with c.id = p_id
       connect by prior c.id = c.parent_id
       order   by pt ) d
    left   join ae_script e    on (e.id = d.id)
    left   join ae_command f   on (f.id = d.id)
    left   join ae_platform p  on (p.id = e.platform_id)
    left   join ae_setting t0  on (t0.object_id  = d.id and 
                                   t0.setting_type_id  = 1000001);
    r_cmd  c_cmd%rowtype;
    cursor c_par(p_cmd_id number) is
    select t.name name, s.value value
    from   ae_script_part a
    inner  join ae_setting s on (s.object_id = a.id)
    inner  join ae_setting_type t on (t.id = s.setting_type_id)
    where  a.id = p_cmd_id
    union  all
    select t.name, s.value
    from   ae_script_part a
    inner  join ae_command c on (c.id = a.id)
    inner  join ae_setting s on (s.object_id = c.template_id)
    inner  join ae_setting_type t on (t.id = s.setting_type_id)
    where  a.id = p_cmd_id;
    r_par  c_par%rowtype;
    l_str  varchar2(1000) default null;
    l_lob  CLOB;
    l_lvl  number default 0;
    l_plv  number default 0;
    l_cnt  number default null;
    l_stt  varchar2(50) default null;
    begin
      delete from ae_script_src where id = p_id;
      insert into ae_script_src(id, text) values (p_id, empty_clob());
      select text into l_lob from ae_script_src where id = p_id;
      dbms_lob.open(l_lob, dbms_lob.lob_readwrite);
      open c_cmd;
      loop
        fetch c_cmd into r_cmd;
        exit when c_cmd%notfound;
        while r_cmd.lv <= l_plv loop
          l_str := '           ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10);
          dbms_lob.writeappend(l_lob, length(l_str), l_str);
          l_plv := l_plv - 1;
          l_lvl := l_lvl - 1;
        end loop;
        l_str := '[' || trim(to_char(r_cmd.id, '000000')) || '] ';
        if not r_cmd.direction is null then
          if r_cmd.direction = '1' then
             l_str := l_str || '<';
          else
             l_str := l_str || '>';
          end if;
        else
          l_str := l_str || ' ';
        end if;
        l_str := l_str || lpad(' ', 2 * r_cmd.lv);
        if not r_cmd.platform is null then
           l_str := l_str || 'platform:' || r_cmd.platform || '; ';
        end if;  
        if not r_cmd.target is null then
           l_str := l_str || 'target:' || r_cmd.target || '; ';
        end if;
        open c_par(r_cmd.id);
        l_stt := null;
        loop
          fetch c_par into r_par;
          exit when c_par%notfound;
          if l_stt is null and r_par.name = g_if_setting then
             l_str := l_str || g_if_statement || ' (' || r_par.value || ') { ';
             l_stt := r_par.name;
          end if;
          if l_stt is null and r_par.name = g_foreach_setting then
             l_str := l_str || g_foreach_statement || ' (' || 
                      r_par.value || ') { ';
             l_stt := r_par.name;
          end if;
        end loop;
        close c_par;
        open c_par(r_cmd.id);
        loop
          fetch c_par into r_par;
          exit when c_par%notfound;
          if l_stt is null or l_stt <> r_par.name then
             if r_par.name <> g_target_setting then
                l_str := l_str || r_par.name || ':' || r_par.value || '; ';
             end if;
          end if;  
        end loop;
        close c_par;
        select count(*) into l_cnt
        from   ae_subscript
        where  parent_id = r_cmd.id;
        if r_cmd.type_id = 1 and l_cnt > 0 then
           if l_stt is null then
              l_str := l_str || '{';
           end if;
           l_lvl := l_lvl + 1;
           l_plv := r_cmd.lv;
        else
           if not l_stt is null then
              l_str := l_str || ' }';
           end if;
           l_plv := r_cmd.lv - 1;
        end if;
        l_str := l_str || chr(13) || chr(10);
        dbms_lob.writeappend(l_lob, length(l_str), l_str);
      end loop;
      close c_cmd;
      while l_lvl > 0 loop
        l_str := '           ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10);
        dbms_lob.writeappend(l_lob, length(l_str), l_str);
        l_lvl := l_lvl - 1;
      end loop;
      dbms_lob.close(l_lob);
      commit;
    exception
      when others then
        if c_cmd%isopen then close c_cmd; end if;
        if c_par%isopen then close c_par; end if;
        if dbms_lob.isopen(l_lob) = 1 then dbms_lob.close(l_lob); end if;
        rollback;
        raise;
    end;
   
end ae_scripting;
/


После построения дерева скрипта connect by запросом, остальное почти тривиально. Некоторые сложности были связаны только с генерацией правильной последовательности процедурных скобок '{' и '}' определяющих вложенность скриптов. Для настроек if_condition и foreach_var генерируются более привычные формы операторов if и foreach.

Вот что получается в результате работы этой хранимки (весь скрипт целиком я не привожу):

[001420]    target:ats.type; foreach (params) {
[003101]      platform:S-12; if (dou_off.dou = 'REDIRECT_NOANSWER') {
[031010] <      text:MODIFY-SUBSCR:DN=K'%s,CFWD=DEACT&CFWDNOR.; var_list:phone;
[001041] >      regexp:(.+); var_list:error_text; is_error:1;
[001008]        platform:M-200; var_list:is_redirect_param = 1;
              }
[003111]      platform:S-12; if (dou_off.dou = 'REDIRECT_BUSY') {
[031110] <      text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDBSUB.; var_list:phone;
[001041] >      regexp:(.+); var_list:error_text; is_error:1;
[001008]        platform:M-200; var_list:is_redirect_param = 1;
              }
[003121]      platform:S-12; if (dou_off.dou = 'REDIRECT_AUTOINF') {
[031210] <      text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDFIXA.; var_list:phone;
[001041] >      regexp:(.+); var_list:error_text; is_error:1;
[001008]        platform:M-200; var_list:is_redirect_param = 1;
              }
[003131]      platform:S-12; if (dou_off.dou = 'REDIRECT') {
[031310] <      text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDUVAR.; var_list:phone;
[001041] >      regexp:(.+); var_list:error_text; is_error:1;
[001008]        platform:M-200; var_list:is_redirect_param = 1;
              }
[003071]      platform:S-12; if (dou_off.dou = 'SET_ALARM_CLOCK') {
[030710] <      text:MODIFY-SUBSCR:DN=K'%s,ALMCALL=DEACT.; var_list:phone;
[001041] >      regexp:(.+); var_list:error_text; is_error:1;
[001009]        var_list:is_alarm_param = 1;
              }
            }

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

<Имя настройки>:<Значение>;

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

Важной, но не обязательной частью скрипта являются числа в квадратных скобках. Это рекомендуемые значения ID для размещения скрипта или команды в БД. Задав одинаковое значение ID для команд или скриптов, можно добиться повторного использования фрагмента скрипта (при условии того, что помеченные фрагменты действительно идентичны), разместив этот фрагмент в БД однократно. Если значение ID не задано, оно назначается автоматически, при загрузке скрипта в БД.

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

В следующей статье, мы займемся реализацией этой возможности.

воскресенье, 25 ноября 2012 г.

Блоха из Мармелада (пара слов об отладке)

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

В качестве примера, рассмотрим приложение s3e/s3eIOSAppStoreBilling, поставляемое в составе examples к Marmalade. Если его запустить, то все что нам удастся увидеть — это модальное окно, с сообщением об ошибке «This example only works on iPhone with a test store set up for in-app purchase». Это конечно правильно, но нам то хочется отладить приложение до того, как мы его разместим на IPhone. Сделать это совсем не сложно. Мы разработаем заглушку, имитирующую (в отладочной сборке) поведение S3E iOS App Store Billing. Начнем с добавления наших файлов в mkb-файл:

#!/usr/bin/env mkb
files
{
    s3eIOSAppStoreBilling.cpp
    AppStoreStub.h
    AppStoreStub.cpp
}
...

Сам код заглушки примитивен. В h-файле для каждой функции App Store Billing мы доопределяем заглушку с суффиксом «Stub» и в случае если сборка отладочная, подменяем вызовы директивой #define:

#ifndef _APPSTORESTUB_H_
#define _APPSTORESTUB_H_

#include <string.h>
#include "s3e.h"#include "s3eIOSAppStoreBilling.h"
 
#define APP_STORE_TIMEOUT     1000
 
#if defined IW_DEBUG
#define s3eIOSAppStoreBillingAvailable   \
               s3eIOSAppStoreBillingAvailableStub
#define s3eIOSAppStoreBillingGetInt  \
               s3eIOSAppStoreBillingGetIntStub
#define s3eIOSAppStoreBillingInit \
               s3eIOSAppStoreBillingInitStub
#define s3eIOSAppStoreBillingStart \
               s3eIOSAppStoreBillingInitStub
#define s3eIOSAppStoreBillingTerminate \
               s3eIOSAppStoreBillingTerminateStub
#define s3eIOSAppStoreBillingStop \
               s3eIOSAppStoreBillingTerminateStub
#define s3eIOSAppStoreBillingRequestProductInformation  \
               s3eIOSAppStoreBillingRequestProductInformationStub
#define s3eIOSAppStoreBillingCancelProductInformationRequests \
               s3eIOSAppStoreBillingCancelProductInformationRequestsStub
#define s3eIOSAppStoreBillingRequestPayment \
               s3eIOSAppStoreBillingRequestPaymentStub
#define s3eIOSAppStoreBillingCompleteTransaction \
               s3eIOSAppStoreBillingCompleteTransactionStub
#define s3eIOSAppStoreBillingRestoreCompletedTransactions \
               s3eIOSAppStoreBillingRestoreCompletedTransactionsStub
#endif

s3eBool   s3eIOSAppStoreBillingAvailableStub();
int32     s3eIOSAppStoreBillingGetIntStub(s3eIOSAppStoreBillingProperty property);
s3eResult s3eIOSAppStoreBillingInitStub(s3eProductInformationCallbackFn infoCallback,  
                                        s3ePaymentTransactionUpdateCallbackFn updateCallback,  
                                        void* userData);
void      s3eIOSAppStoreBillingTerminateStub();
s3eResult s3eIOSAppStoreBillingRequestProductInformationStub(const char** productIdentifiers,  
                                        uint32 numProductIdentifiers);
void      s3eIOSAppStoreBillingCancelProductInformationRequestsStub();
s3eResult s3eIOSAppStoreBillingRequestPaymentStub(s3ePaymentRequest* paymentRequest);
s3eResult s3eIOSAppStoreBillingCompleteTransactionStub(s3ePaymentTransaction*  transaction,  
                                        s3eBool finalise);
s3eResult s3eIOSAppStoreBillingRestoreCompletedTransactionsStub();
void      appStoreStubUpdate();
#endif // _APPSTORESTUB_H_

Реализация этих функций также не содержит ничего сверхестественного:

#include "AppStoreStub.h"

#define PRODUCT_INFO_REQUESTED                               0x0001
#define TRANSACTION_REQUESTED                                0x0002
#define MAX_PRODUCTS_REQUEST                                 5

s3eProductInformationCallbackFn productInfoCallback          = NULL;
s3ePaymentTransactionUpdateCallbackFn transactionCallback    = NULL;
void* billingData                                            = NULL;

uint32 productsCnt = 0;
s3eProductInformation productInformation[MAX_PRODUCTS_REQUEST];
s3ePaymentTransaction transaction;
s3eTransactionReceipt receipt;
uint64 currentTimestamp = 0;
int eventMask = 0;

s3eBool s3eIOSAppStoreBillingAvailableStub() {
    return S3E_TRUE;
}

int32 s3eIOSAppStoreBillingGetIntStub(s3eIOSAppStoreBillingProperty property) {
    switch (property) {
        case S3E_IOSAPPSTOREBILLING_CAN_MAKE_PAYMENTS:
            return 1;
        default:
            return 0;
    }
}

s3eResult s3eIOSAppStoreBillingInitStub(
              s3eProductInformationCallbackFn infoCallback,
              s3ePaymentTransactionUpdateCallbackFn updateCallback,
              void* userData) {
    productInfoCallback = infoCallback;
    transactionCallback = updateCallback;
    billingData = userData;
    return S3E_RESULT_SUCCESS;
}

void s3eIOSAppStoreBillingTerminateStub() {}

s3eResult s3eIOSAppStoreBillingRequestProductInformationStub(
              const char** productIdentifiers,
              uint32 numProductIdentifiers) {
    if (numProductIdentifiers == 0) return S3E_RESULT_ERROR;
    if (numProductIdentifiers >= MAX_PRODUCTS_REQUEST) return S3E_RESULT_ERROR;
    for (uint32 i = 0; i < numProductIdentifiers; i++) {
       memset(&productInformation[i], 0, sizeof(s3eProductInformation));
       strcpy(productInformation[i].m_ProductID, productIdentifiers[i]);
       strcpy(productInformation[i].m_LocalisedTitle, productIdentifiers[i]);
       strcpy(productInformation[i].m_LocalisedDescription, productIdentifiers[i]);
       strcpy(productInformation[i].m_FormattedPrice, "0.00");
       strcpy(productInformation[i].m_PriceLocale, "0.00");
       productInformation[i].m_Price = 0;
       productInformation[i].m_ProductStoreStatus = S3E_PRODUCT_STORE_STATUS_VALID;
    }
    productsCnt = numProductIdentifiers;
    currentTimestamp = s3eTimerGetMs();
    eventMask |= PRODUCT_INFO_REQUESTED;
    return S3E_RESULT_SUCCESS;
}

void s3eIOSAppStoreBillingCancelProductInformationRequestsStub() {}

s3eResult s3eIOSAppStoreBillingRequestPaymentStub(
              s3ePaymentRequest* paymentRequest) {
    memset(&transaction, 0, sizeof(transaction));
    transaction.m_TransactionStatus = S3E_PAYMENT_STATUS_PURCHASED;
    transaction.m_Request = paymentRequest;
    transaction.m_Retain = S3E_FALSE;
    transaction.m_TransactionReceipt = &receipt;
    strcpy(transaction.m_TransactionID, "1");
    strcpy(transaction.m_OriginalTransactionID, "1");
    memset(&receipt, 0, sizeof(receipt));
    receipt.m_ReceiptSize = 0;
    currentTimestamp = s3eTimerGetMs();
    eventMask |= TRANSACTION_REQUESTED;
    return S3E_RESULT_ERROR;  // ???
}

s3eResult s3eIOSAppStoreBillingCompleteTransactionStub(s3e
              PaymentTransaction*  transaction,
              s3eBool finalise) {
    return S3E_RESULT_SUCCESS;
}

s3eResult s3eIOSAppStoreBillingRestoreCompletedTransactionsStub() {
    return S3E_RESULT_SUCCESS;
}

void appStoreStubUpdate() {
#if defined IW_DEBUG
    if (eventMask != 0) {
        if (s3eTimerGetMs() - currentTimestamp < APP_STORE_TIMEOUT) return;
        if (eventMask & PRODUCT_INFO_REQUESTED) {
            for (uint32 i = 0; i < productsCnt; i++) {
                productInfoCallback(&productInformation[i], billingData);
            }
            productsCnt = 0;
        }
        if (eventMask & TRANSACTION_REQUESTED) {
            transactionCallback(&transaction, billingData);
        }
        eventMask = 0;
    }
#endif
}

Здесь следует упомянуть о функции appStoreStubUpdate. Дело в том, что App Store Billing API использует асинхронные вызовы для работы с магазином. Это означает, что вызовы callback функций должны выполняться вне контекста вызова функций запроса информации о продукте s3eIOSAppStoreBillingRequestProductInformation и совершения покупки s3eIOSAppStoreBillingRequestPayment. Эти вызовы мы будем выполнять в appStoreStubUpdate, вызывая последнюю из update-функции Maramalade-приложения. Макрос APP_STORE_TIMEOUT позволяет определять задержку, при обращении к "магазину".

Также, может вызывать вопросы код возврата s3eIOSAppStoreBillingRequestPaymentStub. Честно говоря, мне самому непонятен этот момент. По логике вещей, при успешном выполнении она должна возвращать S3E_RESULT_SUCCESS (равное 0), но в примере используется следующая проверка корректности формирования запроса:

if (s3eIOSAppStoreBillingRequestPayment(&g_PaymentRequest))
     SetStatus("Purchasing %s...", g_ProductID2);
else
     SetStatus("Purchasing %s FAILED", g_ProductID2);

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

Последнее, о чем следует упомянуть, это чек. В s3eIOSAppStoreBillingRequestPaymentStub мы формируем пустой чек:

...
memset(&receipt, 0, sizeof(receipt));
receipt.m_ReceiptSize = 0;
...

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

Осталось совсем немного. В код примера мы добавляем #include AppStoreStub.h (он обязательно должен идти последним) и вызов appStoreStubUpdate, для эмуляции асинхронных вызовов:

bool ExampleUpdate()
{
    appStoreStubUpdate();
    ...

Скомпилировав пример и запустив его под отладчиком, мы можем убедиться, что теперь мы можем запрашивать информацию о продукте и совершать покупки, совершенно не беспокоя при этом App Store.Разумеется, аналогичным образом можно отлаживать Android-приложения, совершающие внутренние покупки, с использованием Android Market Billing.

суббота, 10 ноября 2012 г.

Работа над ошибками

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

Я не сразу это заметил, а затем еще дольше искал, в чем ошибка. Ошибка здесь:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (checkBounce(t->id, msg)) return;
                if (currentScene != NULL) {
                    currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                              t->x, t->y);
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Как всегда, все произошло из за того, что "хотелось как лучше". Дело в том, что если не предпринимать каких-то дополнительных действий, при нажатии, Touchpad (имеется в виду модуль проекта) посылает целую последовательность событий "нажатие", там где кнопке хватило бы всего одного. В результате, кнопка начинала "дребезжать", быстро нажимаясь и отжимаясь. Чтобы этого избежать, была добавлена функция checkBounce, формирующая правильную последовательность событий, убирая лишние. В процессе поиска ошибки, ее код был перепроверен, наверное, раз десять. А ошибка была совсем не там.

Вот правильный код:

void Desktop::update(uint64 timestamp) {
    currentTimestamp = timestamp;
    if (isChanged) {
        getScreenSizes();
    }
    int cnt = touchPad.getTouchCount();
    if (cnt > 0) {
        for (int i = 0; i < MAX_TOUCHES; i++) {
            Touch* t = touchPad.getTouch(i);
            if (t->isActive) {
                int msg = (cnt > 1)?emtMultiTouchUp:emtSingleTouchUp;
                if (t->isMoved) {
                    msg = (cnt > 1)?emtMultiTouchMove:emtSingleTouchMove;
                }
                if (t->isPressed) {
                    msg = (cnt > 1)?emtMultiTouchDown:emtSingleTouchDown;
                }
                if (!checkBounce(t->id, msg)) {
                   if (currentScene != NULL) {
                       currentScene->sendMessage(msg | (t->id & emtTouchIdMask), 
                                                 t->x, t->y);
                   }
                }
            }
        }
    }
    if (isKeyAvailable) {
        s3eKeyboardUpdate();
    }
    if (currentScene != NULL) {
        currentScene->update(timestamp);
    }
}


Найдите десять отличий ;)

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

P.S. В эти выходные закончился Evaluation период у Мармелада. Я решил его продлить, теперь уже за денежку :) Считаю, что $149 совсем не дорого за такое удовольствие.