Sake SDK

From GameSpy SDK

SAKE Persistent Storage SDK

Overview

Sake (pronounced sah-keh, like the Japanese rice wine) is a GameSpy.net service which provides for flexible storage of arbitrary data on the GameSpy backend. This data can be global or player-specific, and it can be accessed or updated by game clients using the Sake SDK. Player-specific data can be private to a specific user, or it can be publicly accessible by all players. A database schema, created by the developer through a webpage interface, is used to organize the data. A range of data types can be stored in the database, including integers, floats, strings, dates and times, and files. Sake is simple to use, however it is a powerful system that can be used to provide game's with a whole new range of functionality.

A developer using Sake will deal with two separate components: the Sake Administration website and the Sake SDK. The Sake Administration site is used to setup the database schema which will store the game's data. The Sake SDK is used by the game to access the database.

File Manifest

The following files should be included with this package. If any of the files are missing, please contact devsupport@gamespy.com.

File
Description
sake.h
GameSpy Sake header (all user functions are prototyped here)
sakeMain.c
Entry point for all user Sake functions
sakeMain.h
Common header for internal code
sakeRequest.c
Code handles and processes the Sake requests
sakeRequest.h
Header for Sake request-handling functions
sakeRequestInternal.h
Another header for Sake request-handling functions
sakeRequestMisc.c
Code to do internal processing of misc Sake requests
sakeRequestModify.c
Code to do internal processing of Sake requests that modify data
sakeRequestRead.h
Code to do internal processing of Sake requests that read data
../common/gsSoap.c
GameSpy Soap code for XML streams
../common/gsSoap.h
Header for GameSpy Soap code
../common/gsXML.c
Code that does XML reading/writing
../common/gsXML.h
Header for GameSpy XML code
../ghttp/
HTTP SDK
/saketest/
A Sake test app written for the command-line in ANSI C

Database

The core of Sake is the database schema used to store a game's data on the backend. A developer sets up the schema for a particular game through the Sake Administration website. The game can then access the database defined by this schema, using the Sake SDK.

The database for each game consists of a set of developer-created tables. Each entry in a table is referred to as a record. Each table has a set of fields, each of which describes a piece of data that will be stored in each of that table's records. In SQL terminology, a field is like a column and a record is like a row.

Example "high_scores" Database Table
recordid ownerid level score
1 7623458 10 514
2 7821536 8 456
3 6998135 23 2678
4 7245991 36 4513
5 7400268 22 2449
6 6701102 10 498

Tables

In Sake, each table is uniquely identified within each game by a short string, called the tableid. For example, the tableid for the above sample table could be "high_scores". Every table contains some number of records, along with a set of fields which defines what values are stored in each record. A table is typically only accessible by a single game; however there is a mechanism on the backend which allows for a table to be shared across two or more games. This could be used to store per-player information that is shared by all of the games in a series or franchise. Developers create and manage tables using the Sake Administration website, and then they can access the data stored in those tables using the Sake SDK. In addition to the tableid, there are a few other important table properties which can be setup using the Sake Administration website.

Owner Type
The owner type is used to designate if each individual table records is owned by a profile (which would identify a particular player), or if all of the records in a table are owned by the backend. If the owner type is set to profile, then each record contains a value which identifies the profile that owns that record. The owner would be the profile for which the record was originally created. The profile owner type is used for tables which store per-player information. If the owner type is set to backend, then the backend owns all of the records in the table. This would typically be used to store general global information which the developer may wish to periodically update. The owner type is a basic table property which must be set when a table is created - it cannot be changed after table creation
Permissions
Table permissions control the ability to create, read, update, or delete records. There are four permissions, public create, public read, owner update, and owner delete, each of which can be set to true or false:
  • public create - if set to true, allows anyone to create records in the table. This will typically be set to true for tables with an owner type of profile, so that clients can create new records, and it will typically be set to false for tables with an owner type of backend.
  • public read - if set to true, then anyone can read any record in the table. If the permission is set to false, then records can only be read by their owners. So this permission can be used to control if player-specific data stored in a table should be public or private.
  • owner update - used to control whether or not a record can be updated by its' owner after it has been created. If the permission is set to true, then an owner can update the record any number of times. This may be used for a table where player preferences are stored. If the permission is set to false, then an owner can only create records, not update them. This may be used for a table in which records are used to record one-off events.
  • owner delete - if set to true, then players can delete records which they have created in the table. If, for example, records in the table correspond to items which the player has collected, then a record could be deleted if the player sells or loses an item. If the permission is set to false, then records cannot be deleted. This could be used in a table where each record stores a player's high scores, which would never be erased.
Rateable
The rateable option controls whether or not records in a table can be rated by users. For example, a racing game may all users to upload replay videos of their best races. Each video is stored in a record in a table. If that table's public read permission is set to true (giving other users access to those videos), and if the table's rateable option is set to true, then users can submit ratings for videos which they like or dislike. Other users can then see a video's average rating and the number of times it has been rated. The rating can also be used by the game to sort or filter a list of videos.
If a table has rateable set to true, then two fields are automatically added to that table. The num_ratings field stores the number of times that users have given that record a rating, and the average_rating field stores the average of all the ratings given to that record. See below for more information on fields. The maximum range for ratings is 0 through 255 (games can internally restrict that to a smaller range). Ratings are given as integers, however the average ratings is returned as a floating point number.
In addition, players can use the field name my_rating to obtain their personal rating on a given record. This can also be used when searching for records. So for example, if you wanted to only view records you have rated as > 100 then you would include "my_rating > 100" in the filter string. For records that the player has not yet rated, my_rating is set to -1 by default.
Limit Per Owner
The limit per owner option is used when users should be prevented from having more than a certain number of records in a table at any one time. If the option is set to 0, then users can have however many records they want. However if the option is set to some number greater than 0, then that number sets the maximum number of records that any user can have in the table. For example, if a table is used to store player preferences, then the limit per owner for that table may be set to 1, since users would only ever have one set of preferences. Another example would be a table in which each record represents a special item that a player's character owns, and the game wants to limit each player to only 5 special items. Then the limit per owner for that table would be set to 5, which will prevent the user from storing more than 5 records in that table. If the user has 5 records in the table, then one of the five records will need to be deleted before adding a new record.

Fields

A field represents a piece of data which is stored in each record in a table. In the above example of a high scores table, there are four fields: recordid, ownerid, level, and score. Each record stored in the table has a value for each of these fields. All tables have one or more fields that are automatically created and managed by Sake. In the example, these are the recordid and ownerid fields. The developer can also add his own fields to any table. In the example, these are the level and score fields.

Developers add and manage fields using the Sake Administration website. A field consists of several pieces of information. First, each field has a name, such as "level", "score", or anything else. A field name must be unique within the table to which it belongs. A field also has a type, which defines what sort of data is stored in that field. Depending on the type, a field might also have a maximum length, and it might have a default value. The table below lists all of the possible field types, along with some information about them.

Sake Field Types
Type Name Range Has Default? Has Max Length?Comments
Byte 0 to 255 Yes No 1 byte unsigned int
Short -32,768 to -32,767 Yes No 2 byte unsigned int
Int -2,147,483,648 to 2,147,483,647 Yes No 4 byte unsigned int
Float -1.79E308 to 1.79E308 Yes No 8 byte floating point num
AsciiString up to 1000 chars Yes Yes String of single-byte chars
UnicodeString up to 1000 chars Yes Yes String of multi-byte chars
Boolean true or false Yes No
DateAndTime 1970 through 2038 Yes No Accurate to 1 second
BinaryData up to 2000 bytes No Yes Arbitrary binary data
FileID n/a No No References an uploaded file - treated as an int by the Sake SDK

Every table has a recordid field which is automatically included when that table is created. The field is an Int, and it is used to uniquely identify each record that is stored in the table. When a record is added to the table a value is automatically assigned to its recordid field by the backend. The recordid can then always be used to identify that record within the table.

If a table is created with its owner type property set to profile, then an ownerid field is automatically added to the table. The field is an Int, and it stores the profile which created the record. The value is filled in automatically by the backend when a record is created, and it never changes as long as that record exists. It is used by the backend to manage access to the record, and it can also be used by the game, through the Sake SDK, to figure out who created a record.

If a table has its rateable property set to true, then two additional fields are automatically added by the backend: num_ratings and average_rating. The num_ratings field is an Int, and it stores the number of times that a particular record has been given a rating by users. The average_rating field is a Float, and it stores the average of all the ratings that have been given to a record.

For a description of other special (non developer-defined) fields, see Appendix II below.

Records

While fields are used to define what sort of data will be stored in a table, the actual data is stored in entries known as records. Each record in a table contains a value for each field in that table. The value stored in the recordid field uniquely identifies a record within the table. Records are accessed using the Sake SDK. They can be created, updated, deleted, or read, depending on the permission properties for each table. More information can be found in the API section below.

Administration

A developer uses the Sake Administration website to configure a game's database schema. This is done by creating tables and then adding fields to those tables. Properties can also be configured for tables and fields. This schema is then used by the game through the Sake SDK. The website is located at: http://tools.gamespy.net/SakeAdmin/.

Starting

When you first visit the Sake Administration website, you will be asked to log in. Sake uses the GameSpyID system, which is the same login system used by the Presence & Messaging SDK (GP), GameSpy.com, GameSpy Arcade, FilePlanet.com, etc. Before using the Sake Administration website, you must be granted permission on the backend. To request permission for your GameSpyID account, send an email to devsupport@gamespy.com.

If you have permission to access your game's Sake Administration website, and you have logged in, then you will be able to start editing your game's database schema. You can do this by selecting your game from the main Game Selection page. This will lead you to the Game Tables page for your game. This page lists all of the tables for your game, and it also allows you to add new tables or edit existing tables.

Tables

The Game Tables page shows any tables that have been created for the selected game, along with each table's properties. The Table ID column shows the short string that uniquely identifies each table within the game's database. The Description column contains a developer supplied comment. This is only used on the Administration website and allows the developer to document the purpose of the table. The Owner Type column indicates if the table has an owner type of profile or backend. The Public Permissions and Owner Permissions columns show the settings for the public create, public read, owner update, and owner delete permissions. See the Database section above for more information about permissions. The Rateable column shows whether or not users can rate records contained in the table. The Limit Per Owner column shows the maximum number of records that any user can have in the table at any one time. If the value is 0, then there is no limit.

There are several buttons to the left of each table. The Edit button can be used to edit that table's properties. All of the properties can be edited, aside from the owner type, which needs to be set when a table is created. When done editing, click Update to save the edit, or Cancel to cancel the edit. The Fields button brings you to a separate page which allows you to edit the list of fields for that table (see below for more information). The Reset button is used to delete all of the records from that table. The Delete button is used to remove a table from the list of table's associated with a game. Note that this won't actually delete the table and its records, but the table will no longer show up in the list, and it will no longer be accessible through the Sake SDK.

The Add a New Table box at the bottom of the page can be used to create a new table. The tableid and the owner type must be specified. All other properties can be set after the table has been created. The tableid can also be changed after the table is created, however the owner type cannot be changed. After entering the tableid and owner type, click the Create Table button to add the table to the list of table's for the current game.

Fields

To view and edit the list of fields for a particular table, click the Fields button to the left of that table on the Game Tables page. This will open up the Game Table Fields page for the selected table. You will see a list containing each of the fields in the table. In addition to any developer created fields, there will be one or more fields that were automatically created by Sake. Every table has a recordid field, which uniquely identifies each record within the table. If a table has an owner type of profile, then there will also be an ownerid field which contains the profile ID of the user that created that record. If a table has its rateable property set to true, then there will two additional fields, num_ratings and average_rating, which are described above.

The Name column shows the field's name, which is unique within the table. The Description column contains a developer supplied comment. This is only used on the Administration website and allows the developer to document the purpose of the field. The Type column shows the type of data that is stored in the field. The database section above has a list of all the types. The Max Length column shows the maximum number of characters for AsciiString and UnicodeString fields and the maximum number of bytes for BinaryData fields. The Default column shows the default value, if the type for that field supports a default value.

There are two buttons to the left of most fields in the list. The Edit button is used to edit that field's properties. The field's name and description can always be edited. The max length and default value can also be edited, if the field's type uses those properties. A field's type cannot be edited after it is created. The Delete button is used to delete that field from the table. The recordid and ownerid fields cannot be edited or deleted. The num_ratings and average_rating fields also cannot be edited or deleted on the Game Table Fields page; however the fields can be removed by setting the table's rateable property to false.

The Add a New Field box at the bottom of the Game Table Fields page is used to add new fields to the current table. First enter a name for the new field, and then select a type from the dropdown box. Enter a max length and/or a default value depending on which type you selected. See the list of types above to see if either is needed. The name, max length, and default value can be changed after the field has been created, but the type can only be set during creation. When ready, click the Add field button to create the field and add it to the list. Once a field has been created, you can enter a description for it in the list.

SDK Implementation

Requirements

As with all GameSpy SDKs, Sake uses the GameSpy Common code. It also relies on the GameSpy HTTP SDK, which it uses to send requests to the Sake backend. The GameSpy Presence and Message SDK (GP) is also needed to provide authentication information for players.

Before using Sake, a game must have first performed the standard GameSpy Availability Check. This ensures that the GameSpy backend is available, and that the current game has access to the backend. See the Sake test app for sample code.

Sake uses the GameSpy Core object, which is part of the Common code, to manage tasks. The game must initialize the Core before using Sake. This is done by calling gsCoreInitialize. In order to allow Sake to process its requests, the core object must be periodically processed by calling gsCoreThink. When the game has finished using Sake it should shutdown the care with gsCoreShutdown. See the Sake test app for sample code for calling these functions.

void gsCoreInitialize();
void gsCoreThink(gsi_time theMs);
void gsCoreShutdown();

Sake needs the GP SDK to provide authentication information for players. This means that for a game to use Sake, it must also use GP. The player must successfully login with GP before using Sake, so that Sake can have access to GP's authentication information.

Field Types

The Sake SDK uses a few basic types to store data regarding fields. To represent a field itself, SAKEField is used.

typedef struct
{
	char         *mName;
	SAKEFieldType mType;
	SAKEValue     mValue;
} SAKEField;

A SAKEField object stores the field's name, the type of data stored in the field, and the value stored in the field. SAKEFieldType is used to indicate the type of data stored in a field.

typedef enum
{
	SAKEFieldType_BYTE,		
	SAKEFieldType_SHORT,		
	SAKEFieldType_INT,		
	SAKEFieldType_FLOAT,		
	SAKEFieldType_ASCII_STRING,	
	SAKEFieldType_UNICODE_STRING,	
	SAKEFieldType_BOOLEAN,		
	SAKEFieldType_DATE_AND_TIME,	
	SAKEFieldType_BINARY_DATA,	

	SAKEFieldType_NUM_FIELD_TYPES
} SAKEFieldType;

It is important to note that all of the field types that can be created through the Administration site are represented here, with the exception of a FileID. That is because FileIDs must be handled specially on the backend, but from the perspective of the SDK they can be treated as Ints. So when reading a FileID field the backend will indicate it is a SAKEFieldType_INT, and when updating a FileID field it should be updated as an SAKEFieldType_INT.

The value for a field is stored in a SAKEValue union.

typedef union
{
	gsi_u8          mByte;
	gsi_i16         mShort;
	gsi_i32         mInt;
	float           mFloat;
	char           *mAsciiString;
	unsigned short *mUnicodeString;
	gsi_bool        mBoolean;
	time_t          mDateAndTime;
	SAKEBinaryData  mBinaryData;
} SAKEValue;

The mType member of the SAKEField object to which this SAKEValue belongs is used to indicate which of the union members contains the actual value for this field. There is a union member corresponding to each of the types in the SAKEFieldType enum. mByte, mShort, mInt, and mFloat simply store integer or floating point values. mAsciiString and mUnicodeString contain pointers to strings which are NUL terminated for ASCII or double-NUL terminated for Unicode. To set the value of mBoolean, use gsi_true and gsi_false. However to check the value of mBoolean, the macros gsi_is_true and gsi_is_false should be used. mDateAndTime contains a date and time value stored in the same format as that returned by the standard time() function. mBinaryData contains arbitrary binary data stored in a SAKEBinaryData struct.

typedef struct
{
	gsi_u8 *mValue;
	int     mLength;
} SAKEBinaryData;

mValue points to the data itself, and mLength contains the number of bytes of data. mValue may be NULL if mLength is 0.

When a SAKEField is supplied to the SDK as part of an input object (described below under requests), then the game is responsible for providing the memory to which any pointers point. The field name and any string or binary data pointers must point to memory which the game is managing. If a SAKEField object is passed to the SDK as an output object (described below under requests), then all the pointers will point to memory which the SDK is managing. This memory should not be freed, and any data which the game wants to access at a later point must be copied.

Startup and Cleanup

Before using Sake, the GameSpy Availability Check must have been performed and indicated that the game's backend is available, and the Core object must have been initialized, as described above. After these steps are completed, and the game is ready to start using Sake, it can call sakeStartup.

SAKEStartupResult SAKE_CALL sakeStartup(SAKE *sakePtr);

The function returns a SAKEStartupResult, which is an enumeration of possible results. If the result is SAKEStartupResult_SUCESS, then the startup has succeeded. Any other value indicates a failure, and the game should not continue calling other Sake functions.

The game supplies a pointer to a SAKE variable when calling sakeStartup. If the startup is successful, then the variable will store a reference to the internal state of the Sake SDK. This SAKE reference is then used with most other calls to Sake functions. The reference is valid until the game shutdowns the Sake SDK with sakeShutdown.

void SAKE_CALL sakeShutdown(SAKE sake);

This shuts down the SDK and frees any memory that was allocated for the Sake object. After this function returns, the reference to the Sake object is no longer valid and should not be used.

After Sake has been shutdown, the game should shutdown the GameSpy Core object by calling gsCoreShutdown. Sample code for this is available in the Sake test app.

Authentication

After Sake has been initialized, the game needs to provide authentication information which will identify the player and game. This allows the backend to ensure that the game can only access or modify information which the current player has permission to access or modify. There are two functions involved in authentication, and the game must call both of them before continuing with any other Sake usage. To set the game's authentication information, call sakeSetGame.

void SAKE_CALL sakeSetGame
(
	SAKE sake, 
	const char *gameName, 
	int gameId
);

The first parameter is the reference to the Sake object obtained when calling sakeStartup. The other two parameters are the gamename and gameid for the current game. These are provided on a per-game basis by GameSpy. If your game needs a gamename and gameid, or if you do not know the gamename or gameid for your game, contact devsupport@gamespy.com.

The function provides no indication of whether or not the gamename and gameid are correct. It only stores them with in the Sake object, and they are then passed along with any requests sent to the Sake backend. The backend will then check them and use the information to figure out which game's database is being used.

void SAKE_CALL sakeSetProfile
(
	SAKE sake, 
	int profileId, 
	const char *loginTicket
);

sakeSetProfile is used to provide authentication information for the current player. The profile ID and login ticket are both obtained from the GameSpy Presence and Messaging SDK (GP). The profile ID uniquely identifies the current player to the backend, and the login ticket allows the backend to verify that the player is correctly identifying himself. Before calling sakeSetProfile, the player should have successfully logged in using the GP SDK, which allows the GameSpy backend to authenticate the player.

The profile ID to pass to sakeSetProfile can be obtained in the callback that is called as a result of logging into GP. A GPConnectResponseArg struct is passed to the callback, and the struct has a member variable "profile" that stores the player's profile ID. While the player is logged on, the game should call the GP function gpGetLoginTicket. This provides the login ticket which is then passed to sakeSetProfile.

As with sakeSetGame, sakeSetProfile provides no indication of whether or not the information provided is correct. It stores the profile ID and login ticket in the Sake object, and they are then passed along with any requests sent to the Sake backend. The backend checks them and uses them to authenticate the player and, for certain requests, identify which player's data is being access or updated.

Requests

To communicate with the Sake backend, the game sends requests through the Sake SDK. This is the primary functionality of the SDK. Once sakeStartup has been called, and the game has provided authentication information (see above), it can start sending requests. Requests allow the game to create records, update records, delete records, read records, and rate records, as well as check the record limit for a particular table (the limit per owner option set with the Administration website).

All of the request functions have a similar format. As an example, this is the function for a CreateRecord request.

SAKERequest SAKE_CALL sakeCreateRecord
(
	SAKE sake, 
	SAKECreateRecordInput *input, 
	SAKERequestCallback callback, 
	void *userData
);

All request functions take a reference to the sake object as the first parameter, a pointer to an input object as the second parameter, a reference to a callback as the third parameter, and a pointer to user data as the last parameter.

The type of the input object parameter is different for each request type - in this case the type is SAKECreateRecordInput. The input object contains the data that will be passed to the backend as part of the request. For a CreateRecord request, the input object contains the tableid of the table in which to create the record and the initial field values to store in the new record. An input object must be valid for the entire duration of a request, which it means it cannot be freed immediately after the request is initiated. It can only be freed if the request fails or after the request completes.

Request functions return a SAKERequest variable, which stores a reference to an internal object that tracks the request. If a request function returns a NULL value, then the request has failed to initialize. If that happens, sakeGetStartRequestResult can be called to get the reason for the failure.

SAKEStartRequestResult SAKE_CALL sakeGetStartRequestResult(SAKE sake);

It returns an enum value of type SAKEStartRequestResult, which will indicate the specific reason. It will always return the result for the most recent request that was attempted, so it must be called immediately after a failure to get the reason for that failure.

All request functions take a reference to a SAKERequestCallback as the third parameter.

typedef void (*SAKERequestCallback)
(
	SAKE sake, 
	SAKERequest request, 
	SAKERequestResult result, 
	void *inputData, 
	void *outputData, 
	void *userData
);

If a request is started successfully, then callback will be called when the request completes. The first two parameters are references to the objects storing the Sake state and the request state. The third parameter is an enum value that indicates success or failure of the request. SAKERequestResult_SUCCESS means success, any other value means failure. The fourth parameter is a pointer to the input object which was passed as the second parameter to the request. The fifth parameter is a pointer to an output object for this request, which will contain any data which the backend sent in response to the request. The specific types for these parameters depend on the type of request. For example, if the request was a CreateRecord request, then the types will be SAKECreateRecordInput and SAKECreateRecordOutput, and the output object will store the recordid of the newly created record. Not all request types has output objects. If a request type does not have an output object, then outputData will be always be NULL when the callback is called. The final parameter is the same user data pointer that was passed to the request function.

The callback is where the game can see the result and any response to its request. If an input object was allocated dynamically, then the game can free that object from within the callback. However it is important to know that for certain request types, the output object may contain pointers to data stored in the input object. Therefore the input object should only be freed at the end of the function, after handling the output object. Also, the output object's data is only valid during the duration of the callback - it cannot be reference after the callback completes. So any data that needs to be accessed later must be copied before the callback returns.

The SDK can make the following requests, each of which follows the format shown in the sakeCreateRecord request above. The only difference is the name of the request, and the Input struct used for each.:

//modifying Records
SAKERequest sakeCreateRecord(..., SAKECreateRecordInput *input, ...);
SAKERequest sakeUpdateRecord(..., SAKEUpdateRecordInput *input, ...);
SAKERequest sakeDeleteRecord(..., SAKEDeleteRecordInput *input, ...);

//retrieving Records
SAKERequest sakeSearchForRecords(..., SAKESearchForRecordsInput *input, ...);
SAKERequest sakeGetMyRecords(..., SAKEGetMyRecordsInput *input, ...);
SAKERequest sakeGetRandomRecord(..., SAKEGetRandomRecordInput *input, ...);
SAKERequest sakeGetSpecificRecords(..., SAKEGetSpecificRecordsInput *input, ...);

//miscellaneous
SAKERequest sakeRateRecord(..., SAKERateRecordInput *input, ...);
SAKERequest sakeGetRecordLimit(..., SAKEGetRecordLimitInput *input, ...);
SAKERequest sakeGetRecordCount(..., SAKEGetRecordCountInput *input, ...);

Thinking

As described above in the Requirements section, the Sake SDK uses the GameSpy Core object to manage requests, so in order for requests to be processed the game must periodically call gsCoreThink.

void gsCoreThink(gsi_time theMS);

gsCoreThink tells the Core object to process any pending tasks. It takes one parameter, which tells it how long it can take to think. You will usually want to pass in 0 as the parameter, which will tell it to let each task do one round of processing.

Generally gsCoreThink will be called once each time a game runs through its main loop, or at a minimum every 50 milliseconds. It only needs to be called while Sake is in use, however calling it more often will not do any harm. It will just return without doing any processing if Sake is not in use. For sample code see the Sake test app.

File References

Sake supports storing files; however the files are not stored directly in the database. To store a file, the game must first upload it to the Sake File Server. If the file is uploaded successfully, the File Server will give the game a fileid which is used to uniquely identify that file, and which can then be stored directly in the Sake database as reference to that file.

The Sake backend keeps track of how many references to each file are stored in the database. If over a period of approximately 24 hours there are no references to a file, it will be deleted from the File server. This ensures that if a fileid is obtained from the database, it will still be valid for a period of time and available for download even if the last reference to that file was removed.

There is no way to overwrite an existing file. To change a file that is referenced from the database, first upload a new file, and then change the fileid in the database to reference the new file. If there are no other references to the old file, it will eventually be deleted.

File Uploading

sakeGetFileUploadURL is used to get a URL which can be used to upload files.

gsi_bool SAKE_CALL sakeGetFileUploadURL
(
	SAKE sake, 
	gsi_char url[SAKE_MAX_URL_LENGTH]
);

The URL will identify both the game and the player that is uploading the file. After obtaining the URL, the file can be uploaded. We recommend using the GameSpy HTTP SDK, however other HTTP SDKs can be used. The file must be uploaded as an HTTP POST. See the Sake test app for a simple example of posting a file.

After completing the post, check the headers returned from the server for the result.

gsi_bool SAKE_CALL sakeGetFileResultFromHeaders
(
	const char *headers, 
	SAKEFileResult *result
);

You can use sakeGetFileResultFromHeaders to do this automatically, or you can check the headers manually for the "Sake-File-Result" header. The value stored in the header is an integer, the possible values of which are enumerated in SAKEFileResult. SAKEFileResult_SUCCESS means that the file was uploaded successfully, while any other value indicates that there was an error uploading the file. Note that sakeGetFileResultFromHeaders returns gsi_true if it was able to find the "Sake-File-Result" header, and gsi_false if it was not able to find the header. So the return value does not in itself indicate that the file was uploaded successfully.

If the file was uploaded successfully, then the fileid that references the file can be obtained using sakeGetFileIdFromHeaders.

gsi_bool SAKE_CALL sakeGetFileIdFromHeaders
(
	const char *headers, 
	int *fileId
);

To get the fileid from the headers manually, look for the "Sake-File-Id" header. The fileid can now be stored in a fileid field in the database. If a file is uploaded, but the fileid is not stored in the database, then the file will be automatically deleted by the backend after approximately 24 hours.

File Downloading

A file can be downloaded once the fileid for that file has been obtained from the Sake database. sakeGetFileDownloadURL is used to get a download URL for a particular fileid.

gsi_bool SAKE_CALL sakeGetFileDownloadURL
(
	SAKE sake, 
	int fileId, 
	gsi_char url[SAKE_MAX_URL_LENGTH]
);

After getting the URL, the file can be download using the GameSpy HTTP SDK, or any other HTTP SDK. After downloading, the headers returned by the server should be checked for the result, to make sure the download was successful.

gsi_bool SAKE_CALL sakeGetFileResultFromHeaders
(
	const char *headers, 
	SAKEFileResult *result
);

You can use sakeGetFileResultFromHeaders to do this automatically, or you can check the headers manually for the "Sake-File-Result" header. The value stored in the header is an integer, the possible values of which are enumerated in SAKEFileResult. SAKEFileResult_SUCCESS means that the file was downloaded successfully, while any other value indicates that there was an error. Note that sakeGetFileResultFromHeaders returns gsi_true if it was able to find the "Sake-File-Result" header, and gsi_false if it was not able to find the header. So the return value does not in itself indicate that the file was downloaded successfully.

Release Process

Games that use Sake are developed using a Sake development backend. This prevents development work from interfering with any live games. When a developer has finished implementing the Sake usage in a game and is ready to start testing the game with the Sake release environment, he should contact GameSpy support at devsupport@gamespy.com.

The developer will need to supply some information on how the game is using Sake, such as what information is being stored in the game's database and how that being accessed and updated. This will allow GameSpy to ensure that the game can be moved to the release environment without any negative impact to that game or to other games. After reviewing this information, GameSpy will move the game's database schema to the release backend. The developer will also need to inform GameSpy if any data should be moved from the development backend to the release backend. The developer will then conduct final testing against the release backend, and GameSpy will monitor the backend to ensure performance.

Appendix I: Default values when creating records with unspecified fields

Sake does not have the concept of NULL data in a record, therefore, if records are created with unspecified fields, the "default" value (listed and set in the Sake Admin site) will be used for these fields upon creating the record.

The only caveat here is for DateAndTime fields. If a DateAndTime field is listed as a defined field when creating a record, but the reported value for this field is NULL, the date will be set to Jan. 01, 1970 by default. If the field is undefined, the default value, getutcdate(), is used.

Appendix II: Special Fields used in Sake

In addition to the fields which the developer defines, you can also request values for special intrinsic fields depending on the data being retrieved. Below is a list of extra fields or search tags that can be requested in Sake:

FileId metadata
FileIds have metadata fields that can be used to obtain extra information about the file. These are formed by following the name of a fileid field with a dot and then a string which controls the metadata to be returned. For example, to get the size of a file stored in a field named "video", you would use the field name "video.size". Note that you can request file metadata without requesting the fileid itself.
  • .size - returns the size of the file in bytes as an Int.
  • .name - returns the name of the file as an AsciiString, as specified when it was uploaded.
  • .create_time - returns a DateAndTime value corresponding to when the file was uploaded.
  • .downloads - returns as an Int the number of times that the file has been downloaded.
  • .profileid - returns as an Int the profileid of the player that uploaded the file.

Rateable Tables
For Rateable tables, two fields are automatically added to that table - these two fields are special in that they cannot be updated manually by a sakeUpdateRecord request (they are updated only by the SDK when rating a record):
  • num_ratings - stores the number of times that users have given that record a rating.
  • average_rating - stores the average of all the ratings given to that record.

The maximum range for ratings is 0 through 255 (games can internally restrict that to a smaller range). Ratings are given as integers, however the average ratings is returned as a floating point number. In addition, players can use the special field tag my_rating to obtain their personal rating on a given record. This can also be used when searching for records. So for example, if you wanted to only view records you have rated as > 100 then you would include "my_rating > 100" in the filter string. For records that the player has not yet rated, my_rating is set to -1 by default.
Lastly, there are two special search tags @rated and @unrated which can be used as SQL filter strings when searching for records to limit the search to rated records (using @rated) or unrated records (using @unrated).

Getting the Row number (or Rank for Leaderboard Queries)

  • row - The row number (based on given sort criteria). Only usable in a sakeSearchForRecords request.

To get a player's Rank, it is as simple as adding the special row field name to the list of fields to retrieve. This gets the row number based on the sort criteria specified.

Appendix III: Reserved Field names

There are some reserved Field Names in Sake that should not be used, otherwise they can cause unforseen problems. these names are: file, fileid, size, name, create_time, downloads, profileid, ownerid, recordid, num_ratings, average_rating, my_rating.

Appendix IV: Using Sake for ATLAS Leaderboard Queries

SAKE has optimized methods for performing basic leaderboard queries of ATLAS Statistics. The following describes their usage.

1. How to get a player's rank (e.g. so you can display "I'm ranked X...")

To get the player's Rank based on the sort order provided, you will add "row" as a special fieldName that returns the player's current row (or in a leaderboard sense, their Rank) to a sakeSearchForRecords query. Note that this row number is relative to the mFilter provided, so this way you can create specific leaderboards (say for example, top 10 players in each gametype). This method works both for getting a single player's rank, or for getting multiple players' ranks.

2. How to get a total record count (e.g. so you can display "... out of X total players")

You can use the sakeGetRecordCount function which can give you the record count for an entire table (no filter) or for a filtered subset of the table (e.g. how many people with stats recorded for a specific gametype, etc.)

Example Usage (gets the total number of players who have accumulated stats):

	static SAKEGetRecordCountInput input;
	static SAKERequest request;
	SAKEStartRequestResult startRequestResult;

	input.mTableId = "PlayerStats_v1";
	input.mFilter = "";
	request = sakeGetRecordCount(sake, &input;, GetRecordCountCallback, NULL);

3. How to get a top 10 leaderboard (or a subset of this, such as top 10-20)

you can use a sakeSearchForRecords query if you want to achieve a top 10 leaderboard for example, and page through it. You will use the parameters: mFilter, mSort, mOffset, mMaxRecords. The offset works to page through the leaderboard.

Example Usage - Top 1-10 in CTF Gametype:

        static SAKEGetRecordCountInput input;
        static SAKERequest request;
        SAKEStartRequestResult startRequestResult;
        static char *fieldNames[] = { "row", "ownerid", "CTF_HighScore" };

        input.mTableId = "PlayerStats_v1";
	input.mFieldNames = fieldNames;
	input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); 
	       
	input.mFilter = "GameType = 'CTF'"; // e.g. filter to a CTF leaderboard only 
	input.mSort = "CTF_HighScore desc"; // e.g. sorts based on a highscore stat
	input.mOffset = 0;
	input.mMaxRecords = 10;
	request = sakeSearchForRecords(sake, &input;, SearchForRecordsCallback, NULL);
Example Usage - Top 11-20 in CTF Gametype:
        // same as above, just change the offset
	input.mOffset = 10;

4. How to get my record and those surrounding me

Again you would use a sakeSearchForRecords query. The parameters this time will include: mTargetRecordFilter, mSurroundingRecordsCount. The mTargetRecordFilter should provide a filter string that will return only a single record, and the mSurroundingRecordsCount is how many records above & below that target record to get as well.

Example Usage - My CTF Record & the surrounding 5 CTF players above and below (11 records total possible):

        static SAKEGetRecordCountInput input;
        static SAKERequest request;
        SAKEStartRequestResult startRequestResult;
        static char *fieldNames[] = { "row", "ownerid", "CTF_HighScore" };

        input.mTableId = "PlayerStats_v1";
	input.mFieldNames = fieldNames;
	input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); 
	
	input.mFilter = "GameType = 'CTF'"; 
	input.mSort = "CTF_HighScore desc";
	input.mOffset = 0;			// offset unused here
	input.mMaxRecords = 11;	
	input.mTargetRecordFilter = "ownerid = 81395321";
        input.mSurroundingRecordsCount = 5;
	
	request = sakeSearchForRecords(sake, &input;, SearchForRecordsCallback, NULL);

5. How to get records for a subset of specific profiles (e.g. get stats/ranks for all my friends)

A sakeSearchForRecords query with parameters now including: mOwnerIds, mNumOwnerIds. The mOwnerIds gives an array of profileids for the subset of records to retrieve, and the mNumOwnerIds indicates the number of ids in the array.

Example Usage - My CTF Record & My buddies' CTF records:

        static SAKEGetRecordCountInput input;
        static SAKERequest request;
        SAKEStartRequestResult startRequestResult;
        static char *fieldNames[] = { "row", "ownerid", "CTF_HighScore" };
        static int ownerIds[5] = { 64880031, 81395342, 81395321, 64880044, 64880040 };

        input.mTableId = "PlayerStats_v1";
	input.mFieldNames = fieldNames;
	input.mNumFields = (sizeof(fieldNames) / sizeof(fieldNames[0])); 
	
	input.mFilter = "GameType = 'CTF'"; 
	input.mSort = "CTF_HighScore desc";
	input.mOffset = 0;			// offset unused here
	input.mMaxRecords = 5;	
        input.mOwnerIds = ownerIds;
        input.mNumOwnerIds = 5;

	request = sakeSearchForRecords(sake, &input;, SearchForRecordsCallback, NULL);