Step by Step: Achievements (Circuitry Documentation)

Introduction

The following article covers a step by step instructional guide how how to integrate Circuitry achievements into your application. It is intended to simplify the achievements code integration process.

Step 1 - Define the Achievements for Your Application

Achievements are specific to your application and are setup on what we refer to as the 'Achievement Configuration' page in the Robot Cache Partner Portal.

Step 2 - Encapsulating Achievements Scope of Work

The following code is independent of any specific application and can be added as you see fit. The class is fully functional on its' own is but can be easily extended to meet any further needs depending on your application.

Header File

First, a structure needs to be defined to hold the achievement data which will be received from Robot Cache and provides a macro for creating objects of that type. This data maps directly to what is found on the 'Achievement Configuration' in the Robot Cache Partner Portal.

#define _ACH_ID( id, name ) { id, #id, name, "", 0, 0 }
struct Achievement_t
{
    int m_eAchievementID;
    const char *m_pchAchievementID;
    char m_rgchName[128];
    char m_rgchDescription[256];
    bool m_bAchieved;
    int m_iIconImage;
};

Next a helper class needs to be defined which will 'wrap' all of the Circuitry statistics API calls as well as create the Circuitry callbacks.

class CCircuitryAchievements
{
private:
    int64 m_iAppID; // Our current AppID
    Achievement_t *m_pAchievements; // Achievements data
    int m_iNumAchievements; // The number of Achievements
    bool m_bInitialized; // Have we called the Request stats function and received a callback?

public:
    CCircuitryAchievements(Achievement_t *Achievements, int NumAchievements);
    ~CCircuitryAchievements();

    bool RequestStats();
    bool SetAchievement(const char* ID);

    CIRCUITRY_CALLBACK( CCircuitryAchievements, OnUserStatsReceived, UserStatsReceived_t,
        m_CallbackUserStatsReceived );
    CIRCUITRY_CALLBACK( CCircuitryAchievements, OnUserStatsStored, UserStatsStored_t,
        m_CallbackUserStatsStored );
    CIRCUITRY_CALLBACK( CCircuitryAchievements, OnAchievementStored,
        UserAchievementStored_t, m_CallbackAchievementStored );
};

Code File

Constructor

Parameters - The constructor takes a pointer to an array of achievements along with the length of the array. The formating will be covered later.
Returns - N/A
What it does - The constructor initializes a number of members along with obtaining the AppID currently running. This also connects the 'call back' methods to handle asynchronous calls made to Circuitry. And finally it makes an initial call to RequestStats() to get stats and achievements for the current user.

CCircuitryAchievements::CCircuitryAchievements(Achievement_t *Achievements, int NumAchievements):
 m_iAppID( 0 ),
 m_bInitialized( false ),
 m_CallbackUserStatsReceived( this, &CCircuitryAchievements::OnUserStatsReceived ),
 m_CallbackUserStatsStored( this, &CCircuitryAchievements::OnUserStatsStored ),
 m_CallbackAchievementStored( this, &CCircuitryAchievements::OnAchievementStored )
{
     m_iAppID = CircuitryUtils()->GetAppID();
     m_pAchievements = Achievements;
     m_iNumAchievements = NumAchievements;
     RequestStats();
}
RequestStats()

Parameters - None
Returns - a boolean which represents whether the call succeeded or not. If it failed then in most cases it means Circuitry is not initialized. Please ensure you have the Robot Cache client open when making this call and that the CircuitryAPI_Init function has been called before it.
What it does - This method wraps a call to the ICircuitryUserStats::RequestCurrentStats function which is an asynchronous call to Robot Cache requesting the statistics and achievements of the current user. Please note that this function call needs to be made BEFORE you are able to set any parameters. The initial function call to this method is made in the 'constructor'. You may subsequently call it again any time if you wish to check on updated stats or achievements.

bool CCircuitryAchievements::RequestStats()
{
    // Is Circuitry loaded? If not we can't get stats.
    if ( NULL == CircuitryUserStats() || NULL == CircuitryUser() )
    {
        return false;
    }
    // Is the user logged on?  If not we can't get stats.
    if ( !CircuitryUser()->LoggedOn() )
    {
        return false;
    }
    // Request user stats.
    return CircuitryUserStats()->RequestCurrentStats();
}
SetAchievement()

Parameters - The string identifier of the achievement that you wish to set (ie. "ACH_WIN_ONE_GAME")
Returns - a boolean representing whether or not the call succeeded. If it failed then either a) Circuitry is not initialized or b) you have not processed the callback from the initial call to the RequestStats function. You will be unable to set any achievements until that callback has been received.
What it does - This method sets a given achievement to 'achieved' status and sends the results to Circuitry. You may set any particular achievement numerous times so there's no need to be concerned about only setting achievements that aren't already set. This is an asynchronous function call which will trigger two callbacks: OnUserStatsStored() and OnAchievementStored().

bool CCircuitryAchievements::SetAchievement(const char* ID)
{
    // Have we received a call back from Circuitry yet?
    if (m_bInitialized)
    {
        CircuitryUserStats()->SetAchievement(ID);
        return CircuitryUserStats()->StoreStats();
    }
    // If not, then achievements are not able to be set as of yet
    return false;
}
OnUserStatsReceived()

Parameters - N/A
Returns - Nothing
What it does - This method is a callback that is called anytime you try to request the statistics. Statistics and achievements are requested by using the RequestStats() function. This method updates the member 'variable m_pAchievements' to reflect the latest data returned from Circuitry.

void CCircuitryAchievements::OnUserStatsReceived( UserStatsReceived_t *pCallback )
{
    // we could get callbacks for other games' stats that may be arriving, but you can ignore them
    if ( m_iAppID == pCallback->m_nGameID )
    {
        if ( k_EResultOK == pCallback->m_eResult )
        {
            OutputDebugString("Received stats and achievements from Circuitry\n");
            m_bInitialized = true;

            // load achievements
            for ( int iAch = 0; iAch < m_iNumAchievements; ++iAch )
            {
                Achievement_t &ach = m_pAchievements[iAch];

                CircuitryUserStats()->GetAchievement(ach.m_pchAchievementID, &ach.m_bAchieved);
                _snprintf( ach.m_rgchName, sizeof(ach.m_rgchName), "%s",
                    CircuitryUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID,
                    "name"));
                _snprintf( ach.m_rgchDescription, sizeof(ach.m_rgchDescription), "%s",
                    CircuitryUserStats()->GetAchievementDisplayAttribute(ach.m_pchAchievementID,
                    "desc"));
            }
        }
        else
        {
            char buffer[128];
            _snprintf( buffer, 128, "RequestStats - failed, %d\n", pCallback->m_eResult );
            OutputDebugString( buffer );
        }
    }
}
OnUserStatsStored()

Parameters - N/A
Returns - Nothing
What it does - This method is a callback that is called anytime you attempt to store statistics in Circuitry.

void CCircuitryAchievements::OnUserStatsStored( UserStatsStored_t *pCallback )
{
    // we may get callbacks for other games' stats arriving, you can ignore them
    if ( m_iAppID == pCallback->m_nGameID )
    {
        if ( k_EResultOK == pCallback->m_eResult )
        {
            OutputDebugString( "Stored stats for Circuitry\n" );
        }
        else
        {
            char buffer[128];
            _snprintf( buffer, 128, "StatsStored - failed, %d\n", pCallback->m_eResult );
            OutputDebugString( buffer );
        }
    }
}
OnAchievementStored()

Parameters - N/A
Returns - Nothing
What it does - This method is a callback that is called anytime achievements are successfully stored on Circuitry.

void CCircuitryAchievements::OnAchievementStored( UserAchievementStored_t *pCallback ) 
{ 
    // we may get callbacks for other games' stats arriving, you can ignore them 
    if ( m_iAppID == pCallback->m_nGameID ) 
    { 
        OutputDebugString( "Stored Achievement for Circuitry" ); 
    } 
} 

Step 3 - Integrating into your Application

Following is a listing of code 'snippets' that will be needed to integrate into an application in the appropriate locations.

Defines and Globals

The following is the list of 'includes' that are required to build with Achievements; an 'enum' of game specific achievements and a global pointer to the helper object. Please note that the achievements must be identical to those that are in the 'Admin page' on Circuitry.

... 
#include "circuitry_api.h" 
// Defining the achievements 
{ 
    ACH_WIN_ONE_GAME = 0, 
    ACH_WIN_100_GAMES = 1, 
    ACH_TRAVEL_FAR_ACCUM = 2, 
    ACH_TRAVEL_FAR_SINGLE = 3, 
}; 

// This is the achievement array which will hold data about the achievements and their state 
Achievement_t g_Achievements[] = 
{ 
    _ACH_ID( ACH_WIN_ONE_GAME, "Winner" ), 
    _ACH_ID( ACH_WIN_100_GAMES, "Champion" ), 
    _ACH_ID( ACH_TRAVEL_FAR_ACCUM, "Interstellar" ), 
    _ACH_ID( ACH_TRAVEL_FAR_SINGLE, "Orbiter" ), 
}; 

// Global access to Achievements object 
CCircuitryAchievements* g_CircuitryAchievements = NULL; 
... 

Initialization

The call to the CircuitryAPI_Init function initializes all of Circuitry and MUST be called before anything else. If that function call succeeds then we can create the 'helper' object by passing in the array of the achievements along with the size of the array.

... 
// Initialize Circuitry 
bool bRet = CircuitryAPI_Init(); 
// Create the CircuitryAchievements object if Circuitry was successfully initialized 
if (bRet) 
{ 
    g_CircuitryAchievements = new CCircuitryAchievements(g_Achievements, 4); 
} 
... 

Processing Callbacks

To ensure that all Circuitry callbacks are called we need to regularly check for any new messages. This is achieved by implementing the following to the game loop code.

... 
CircuitryAPI_RunCallbacks(); 
... 

Triggering Achievements

To trigger an achievement you can issue a simple call along with passing in the achievement identifier.

... 
if (g_CircuitryAchievements) 
    g_CircuitryAchievements->SetAchievement("ACH_WIN_100_GAMES"); 
... 

Shutdown

The call to the CircuitryAPI_Shutdown function will most likely have already been used somewhere in you application as it shuts down Circuitry (NOTE: This MUST be called before exiting your application. Finally, you will nee to delete the 'helper' object.

... 
// Shutdown Circuitry 
CircuitryAPI_Shutdown(); 
// Delete the CircuitryAchievements object 
if (g_CircuitryAchievements) 
    delete g_CircuitryAchievements; 
... 

Step 4 - Testing and Troubleshooting

This sample code will generate debug information to the console that may help you better understand which function calls are succeeding or failing. The following are some common failure messages and fixes:

This application has failed to start because circuitry_api.dll was not found. Re-installing the application may fix this problem.
Make sure that the circuitry_api.dll is in the same directory as the executable file.

[S_API FAIL] CircuitryAPI_Init() failed; unable to locate a running instance of Circuitry, or a local RCServices.dll
In most cases this means that the Circuitry client is not running. You will need to start the Robot Cache Client Application and log in.

[S_API FAIL] CircuitryAPI_Init() failed; no appID found.
In most cases this means that the 'circuitry_appid.txt' file in the proper directory. You will need to put it in your source folder and ensure that it has the proper 'gameId'.