question

lmgualandi avatar image
lmgualandi asked

Turn based game data storage

Very satisfied so far with PlayFab. There's one subject I need advice though.

I have a simple multiplayer turn-based asynchronous game system. I need to store a json string somewhere to be accessed by exactly 2 players for each match created.

There are various methods of storing things in PlayFab, I'm not sure which one is the most appropriate.

Are shared groups appropriate for this kind of use? May it present performance issues when over-used?

There is also the option to store the data in the player that created the match. A player may have multiple games at the same time, I don't know if this is the correct purpose of this feature.

I also plan to keep the match history of each player for stats purposes. This can get large. Some players have 1k+ matches

Right now I'm storing all this in my old server. I'd like to know if it's worth/possible moving it to PlayFab and which method to use. Or should I wait for the official PlayFab turn based multiplayer support?

Player DataShared Group Data
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

brendan avatar image
brendan answered

Either (Shared Group Data or User Read-Only/Internal Data) would work, though it's important to note that you should not add the users directly to the Shared Group Data is you use that, since that would give the users read/write access to the data directly (allowing a hacker to post whatever he wants).

Now, we do limit the number of Shared Group Data IDs in use in your title to 10 per player, and User Data (in all forms) has a limit on the number of keys - after all, nothing is unlimited, and we need to keep the service affordable for everyone. So if you want to store all the data about all the games for all time, you would need to do one of the following:

1. Set up an S3 bucket as an endpoint for the PlayStream Event Archive. That will get you 100% of all events for your game, so if you make each move (and then each game end report) a custom event, you'll have everything. Since you're using it for stats purposes, this is likely your best bet, since it will allow you to do full analysis on everything - each move made, time between moves, etc., etc.

2. Use a custom game server, so that you can write all the gameplay history to the Content service. The Content service isn't limited, since we pass on the costs for it directly (it's CloudFront).

I wouldn't recommend waiting on our turn-based service. That work has not been scheduled yet, so I can't guarantee when it will be available.

8 comments
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

lmgualandi avatar image lmgualandi commented ·

When you say I should not add the users directly, do you mean I should do it via cloud script?

I know there is a limited number of keys for the user data, but is there a size limit for each key?

0 Likes 0 ·
brendan avatar image brendan lmgualandi commented ·

I'm referring specifically to Server/AddSharedGroupMembers. Adding players that way gives them the ability to change any data from the client.

And yes, the Value for a Key in Shared Group Data is limited to 1,000 bytes.

0 Likes 0 ·
lmgualandi avatar image lmgualandi commented ·

How about player title data? 1KB isn't enough

0 Likes 0 ·
brendan avatar image brendan lmgualandi commented ·

You can increase the size limit for values in Shared Group Data via the Add-ons->Limits tab.

User Title Data is limited to 10,000 bytes by default, which you can also upgrade, but bear in mind that it's specific to the user, so you would need to either save it on each user, or save a reference to the other user's data on one of them. As with Shared Group Data, the other thing to bear in mind is that you do have a limit on the total number of keys you can write to - 10, by default - so you do need to make sure you're only keeping the most recent game sessions (though again, you can always collect the information directly, for long-term storage of all games).

0 Likes 0 ·
katamaran avatar image katamaran commented ·

"I wouldn't recommend waiting on our turn-based service. That work has not been scheduled yet, so I can't guarantee when it will be available."

when this features come?

0 Likes 0 ·
brendan avatar image brendan katamaran commented ·

Until something is in active development, we do not estimate completion dates. As a live service, we have to adapt to whatever conditions exist for our live titles as our highest priority, and that can move our schedule around quite a bit. We would not want anyone to take a dependency on a feature for which we aren't 100% confident of the date.

0 Likes 0 ·
katamaran avatar image katamaran brendan commented ·

any sugestion for turn based game documantation, tutorial or sample project?

0 Likes 0 ·
Show more comments
bda2206@gmail.com avatar image
bda2206@gmail.com answered

@lmgualandi

my work in progress node.js turn based game server

var express = require('express');
var bodyParser = require('body-parser');
var loki = require("lokijs");
var util = require('util');
var https = require('https');
var querystring = require('querystring');
var server = express();
var router = express.Router(); 
var games;
server.use(bodyParser.urlencoded({ extended: true }));
server.use(bodyParser.json());
router.get('/', function(req, res) {
    res.json({ message: 'hooray! welcome to our api!' });   
});
var Playfab = {
    titleId: "",
    serverAPIKey : "",
    PublisherID : "",
    sdkVersion: "0.13.160328",

    ServerUrl : "????.playfabapi.com"
    
}
function OnUpdateSharedGroupData(res,callbackData)
{
	console.log('OnUpdateSharedGroupData');
	res.setEncoding('utf8');
	res.on('data', function (chunk) {
		console.log('Playfab Response: ' + chunk);
		var result = JSON.parse(chunk);
		if (result){
			if (result.status == "OK"){
				//add to the list of games available in DB
				var newGame = {
					PlayerID : callbackData.PlayerID,
					SharedGroupID : callbackData.sharedGroupID,
					PlayerSkill : callbackData.PlayerSkill
				}
				console.log("newgame:");
				games.insert(newGame);
				console.log("setheaders 45:");
				callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
				callbackData.rtc.end('Success');   
			}
			else{
				console.log("setheaders 50:");
				console.log("ERROR onUpdateUserData failed:");
				callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
				callbackData.rtc.end('Fail');      
				return false;
				}
		}


	});   
}
function OnAddSharedGroupToPlayersUserData(res,callbackData){
    console.log('OnAddSharedGroupToPlayersUserData');
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log('Playfab Response: ' + chunk);
        var result = JSON.parse(chunk);
        if (result){
            if (result.status == "OK"){
                //add to the list of games available in DB
                var newGame = {
                    PlayerID : callbackData.PlayerID,
                    SharedGroupID : callbackData.sharedGroupID,
                    PlayerSkill : callbackData.PlayerSkill
                }
				console.log("newgame:" + newGame);
                games.insert(newGame);
				
				var requestData = '{ a json object ! }';
				console.log(requestData);
				PlayfabExecuteClientRequest("UpdateSharedGroupData",
						requestData,
						callbackData,
						OnUpdateSharedGroupData);        
            }
            else{
				console.log("setheaders 91:");
                console.log("ERROR onUpdateUserData failed:");
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Fail');      
                return false;
            }
        }
          
    });   
}


function AddSharedGroupToPlayersUserData(callbackData){
    console.log("AddSharedGroupToPlayersUserData");
    //turn gamelist into json, then add sharedgroupID, then update users data.??
    console.log(callbackData.gameList);
    if (callbackData.gameList == undefined){
        var newGameList = callbackData.sharedGroupID;
        //var newGameList = JSON.stringify(gameList);
        console.log("was undefined,now:" + newGameList);
        //create a new list
    }else{
            var newGameList = callbackData.gameList + "," + callbackData.sharedGroupID ;
            console.log("was defined, now:" + newGameList);
        }
    
    //callbackData["GameList"] = newGameList;
    var requestData = '{"Data" : {"GameList" : "' + newGameList + '"}}';
    //console.log(requestData);
    PlayfabExecuteClientRequest("UpdateUserData",
                                requestData,
                                callbackData,
                                OnAddSharedGroupToPlayersUserData);
}
function OnGetGameListToAddNewGroup(res,callbackData) {
      console.log('OnGetGameListToAddNewGroup');
      res.setEncoding('utf8');
      res.on('data', function (chunk) {
          console.log('Playfab Response: ' + chunk);
          var result = JSON.parse(chunk);
          if (result){
              if (result.status == "OK"){
                  
                  var gameList = result.data.Data.GameList.Value;
                  console.log("gameList " + gameList)
                  callbackData["gameList"] = gameList;
                  AddSharedGroupToPlayersUserData(callbackData);          
              }
            else{
                console.log("ERROR OnGetGameListToAddNewGame chunk unparsed:" + chunk);
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Fail');   
            }
          }
          
      });   
}


function DelSharedGroupFromPlayersUserData(playerID,
                                            sessionTicket,
                                            sharedGroupID,
                                            callback){


}
function OnCreateSharedGroup(res,callbackData) { 
    //do this with the players token, prove auth is ok
    console.log('OnCreateSharedGroup');
      	res.setEncoding('utf8');
      	res.on('data', function (chunk) {
          	console.log('Playfab Response: ' + chunk);
          	var result = JSON.parse(chunk);
          	if (result){
              	if (result.status == "OK"){
					var sharedGroupID = result.data.SharedGroupId;
					callbackData["sharedGroupID"] = sharedGroupID;
					//GetGameListToAddNewGroup(callbackData);
					var requestData = '{ "Keys" : ["GameList"] }';
					//var postData = querystring.stringify(requestData);
					//console.log(postData);
					PlayfabExecuteClientRequest('GetUserData',
											  requestData,
											  callbackData,
											  OnGetGameListToAddNewGroup);
              }
          }
          
      });
}
function OnAddSharedGroupToPlayer2sUserData(res,callbackData){
    console.log('onUpdateUserData');
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log('Playfab Response: ' + chunk);
        var result = JSON.parse(chunk);
        if (result){
            if (result.status == "OK"){
                //remove from the list of games available in DB
				//var results = games.findOne({ SharedGroupID: {'$eq': callbackData.SharedGroupID} });
                
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Success');          
            }
            else{
                console.log("ERROR onUpdateUserData failed:");
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Fail');      
                return false;
            }
        }
          
    });   
}
function AddSharedGroupToPlayer2sUserData(callbackData){
    console.log("AddSharedGroupToPlayer2sUserData");
    //turn gamelist into json, then add sharedgroupID, then update users data.??
    console.log(callbackData.GameList);
	 var newGameList = "";
    if (callbackData.GameList == undefined){
        newGameList = callbackData.SharedGroupID;
        //var newGameList = JSON.stringify(gameList);
        console.log("was undefined" + newGameList);
        //create a new list
    }else{
        newGameList = callbackData.GameList + "," + callbackData.SharedGroupID ;
        console.log("was defined" + newGameList);
    }
    
    //callbackData["GameList"] = newGameList;
    var requestData = '{"PlayFabId" : "'+callbackData.PlayerID +'", "Data" : {"GameList" : "' + newGameList + '"}}';
    console.log(requestData);
    PlayfabExecuteServerRequest("UpdateUserData",
                                requestData,
                                callbackData,
                                OnAddSharedGroupToPlayer2sUserData);
}
function OnGetGameList2ToAddNewGroup(res,callbackData) {
      console.log('OnGetGameList2ToAddNewGroup');
      res.setEncoding('utf8');
      res.on('data', function (chunk) {
          console.log('Playfab Response: ' + chunk);
          var result = JSON.parse(chunk);
          if (result){
              if (result.status == "OK"){
                  
                  var gameList = result.data.Data.GameList.Value;
                  console.log("GameList " + gameList)
                  callbackData["GameList"] = gameList;
                  AddSharedGroupToPlayer2sUserData(callbackData);          
              }
            else{
                console.log("ERROR OnGetGameListToAddNewGame chunk unparsed:" + chunk);
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Fail');   
            }
          }
          
      });   
}
function OnUpdateSharedGroupPlayer2(res,callbackData){
    //add the game to player2s list of games
    var requestData = '{ "PlayFabId" : "'+callbackData.PlayerID +'", "Keys" : ["GameList"] }';
    //var postData = querystring.stringify(requestData);
    //console.log(postData);
    PlayfabExecuteServerRequest('GetUserData',
                              requestData,
                              callbackData,
                              OnGetGameList2ToAddNewGroup)
}
function DeleteInvalidSharedGroupData(res,callbackData){
	  console.log('DeleteInvalidSharedGroupData');
      res.setEncoding('utf8');
      res.on('data', function (chunk) {
          console.log('Playfab Response: ' + chunk);
          var result = JSON.parse(chunk);
          if (result){
              if (result.status == "OK"){
                  NewGameRequested(callbackData);        
              }
            else{
                console.log("ERROR DeleteInvalidSharedGroupData chunk unparsed:" + chunk);
                callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
                callbackData.rtc.end('Fail');   
            }
          }
          
      }); 
}
function OnGetSharedGroupInfo(res,callbackData){
	console.log('OnGetSharedGroupInfo');
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
    	console.log('Playfab Response: ' + chunk);
        var result = JSON.parse(chunk);
        if (result){
        	if (result.status == "OK"){
				//check game is valid
				if ( result.data.Data.Moves != null && result.data.Data.P1 != null && result.data.Data.P2 != null && result.data.Data.GameData != null){
					var WhosTurn = result.data.GameData;
					if (WhosTurn == "WaitingForPlayer"){
						  //add player2 and update whosturn
							var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'","Data" : {"GameData" : "'+callbackData.PlayerID  +'", "Player2" :"'+callbackData.PlayerID  +'"} }';
					  }else{
							var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID + '",'+
												'"Data" : {"P2" :"'+ callbackData.PlayerID  +
																	'1-2-3-4-5-6-7-'+
																	callbackData.PlayerName+'"} }'; 
						  //"WaitingForPlayer1-2-3-4-5-6-7-Waiting For Player
					  }
					  PlayfabExecuteServerRequest('UpdateSharedGroupData',
													  requestData,
													  callbackData,
													  OnUpdateSharedGroupPlayer2);						  
					}


            }else{
				var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID + '"}';
				PlayfabExecuteServerRequest('DeleteInvalidSharedGroupData',
							  requestData,
							  callbackData,
							  OnDeleteInvalidSharedGroupData);	
				
				
			}
          }
          
    });
}
function OnDeleteSharedGroup(res,callbackData){
	callbackData.rtc.writeHead(200, {'Content-Type': 'text/plain'});
	callbackData.rtc.end('Fail');
}
function OnAddSharedGroupMember(res,callbackData){
    console.log('OnAddSharedGroupMember');
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
          console.log('Playfab Response: ' + chunk);
          var result = JSON.parse(chunk);
          if (result){
              if (result.status == "OK"){
                  //var SharedGroupID = callbackData.SharedGroupID;
                  //callbackData["SharedGroupID"] = SharedGroupID;
                  //get whos turn 
                  var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'",                  "Keys" : ["Moves","P1","P2","GameData"]}';
                  PlayfabExecuteServerRequest('GetSharedGroupData',
                                                  requestData,
                                                  callbackData,
                                                  OnGetSharedGroupInfo);
              }
			  else{
				  var requestData = '{ "SharedGroupId" :"'+ callbackData.SharedGroupID +'"}';
				  PlayfabExecuteServerRequest("DeleteSharedGroup",
											 requestData,
											  callbackData,
											 OnDeleteSharedGroup);
   
				}
          }
          
    });
    


}
function onGetAccountInfoForPlayer2(res,callbackData){
	//used to confirm auth of player 2.
	console.log('onGetAccountInfoForPlayer2');
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
    	console.log('Playfab Response: ' + chunk);
        var result = JSON.parse(chunk);
        if (result){
        	if (result.status == "OK"){
              	var requestData = '{ "SharedGroupId" : "'+ callbackData.SharedGroupID +'","PlayFabIDs"     : ["' + callbackData.PlayerID+'"] }';
            	PlayfabExecuteServerRequest('AddSharedGroupMembers',
                      requestData,
                      callbackData,
                      OnAddSharedGroupMember);
              }
			  else{
				  //unauth - fail message
			  }
          }
          
    });
}
function PlayfabExecuteServerRequest(request,requestData,callbackData,callback){
    var postData = querystring.stringify(requestData);
	console.log("ServerRequest:" + request );
	console.log(requestData);
    var postOptions = {
      host: Playfab.ServerUrl ,
      port: '443',
      path: '/Server/' + request,
      method: 'POST',
      headers: {
            'Content-Type': 'application/json; charset=utf-8',
            'X-PlayFabSDK': 'JavaScriptSDK-' + Playfab.sdkVersion,
            'X-SecretKey' : Playfab.serverAPIKey
      }
  };
     // Set up the request
  var postReq = https.request(postOptions, function(res) {
      //console.log('postReq callback!');
      callback(res,callbackData);


  });
  //  console.log('postReq callback!');
  // post the data
  postReq.write(requestData);
  postReq.end();
}
function PlayfabExecuteClientRequest(request,requestData,callbackData,callback){
    var postData = querystring.stringify(requestData);
	console.log("ClientRequest:" + request );
	console.log(requestData);
    var postOptions = {
      host: Playfab.ServerUrl ,
      port: '443',
      path: '/Client/' + request,
      method: 'POST',
      headers: {
            'Content-Type': 'application/json; charset=utf-8',
            'X-PlayFabSDK': 'JavaScriptSDK-' + Playfab.sdkVersion,
            'X-Authorization' : callbackData.Token
      }
  };
     // Set up the request
  var postReq = https.request(postOptions, function(res) {
      //console.log('postReq callback!');
      callback(res,callbackData);


  });
  //  console.log('postReq callback!');
  // post the data
  postReq.write(requestData);
  postReq.end();
}
function NewGameRequested(callbackData)
{
	    var results = games.findOne({ PlayerID: {'$ne': req.body.PlayerID} });
        if (results == null){
			console.log("start a game");
            var requestData = "";
            callbackData = {
                PlayerID : req.body.PlayerID,
				PlayerName : req.body.PlayerName, 
                Token    : req.body.Token,
                rtc      : res  //response to client
            }    
            PlayfabExecuteClientRequest('CreateSharedGroup',
                                  requestData,
                                  callbackData,
                                  OnCreateSharedGroup);    
        }
        else{
			console.log("found a game");
            console.log(results);
            var SharedGroupID = results.SharedGroupID;
            console.log(SharedGroupID);
            callbackData = {
				SharedGroupID : SharedGroupID,
                PlayerID : req.body.PlayerID,
				PlayerName : req.body.PlayerName, 
				Token    : req.body.Token,
                rtc      : res  //response to client
            }
			//delete the game from the server now to avoid double allocation!
			games.remove(results);
            var requestData = '';
            PlayfabExecuteClientRequest('GetAccountInfo',
                      requestData,
                      callbackData,
                      onGetAccountInfoForPlayer2);
		}
}
router.route('/newgame')
    .post(function(req,res){
	console.log("----------------------------------------------------------------------");
    console.log('post!' + util.inspect(req.body, false, null)); 
    //validate the request.body for playerID and Token
    if ((req.body.PlayerID)&& (req.body.PlayerName) && (req.body.Token)&& (req.body.PlayerSkill))
        {
        var results = games.findOne({ PlayerID: {'$ne': req.body.PlayerID} });
        if (results == null){
			console.log("start a game");
            var requestData = "";
            var callbackData = {
                PlayerID : req.body.PlayerID,
				PlayerName : req.body.PlayerName, 
                Token    : req.body.Token,
                rtc      : res  //response to client
            }    
            PlayfabExecuteClientRequest('CreateSharedGroup',
                                  requestData,
                                  callbackData,
                                  OnCreateSharedGroup);    
        }
        else{
			console.log("found a game");
            console.log(results);
            var SharedGroupID = results.SharedGroupID;
            console.log(SharedGroupID);
            var callbackData = {
				SharedGroupID : SharedGroupID,
                PlayerID : req.body.PlayerID,
				PlayerName : req.body.PlayerName, 
				Token    : req.body.Token,
                rtc      : res  //response to client
            }
			//delete the game from the server now to avoid double allocation!
			games.remove(results);
            var requestData = '';
            PlayfabExecuteClientRequest('GetAccountInfo',
                      requestData,
                      callbackData,
                      onGetAccountInfoForPlayer2);
		}
    }
    else{
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('invalid post data');
    }
});
server.use('/api', router);


var db = new loki('data.db',
     {
        autoload: true,
        autoloadCallback : loadHandler,
        autosave: true, 
        autosaveInterval: 5000
      });


function loadHandler() {
  // if database did not exist it will be empty so I will intitialize here
  games = db.getCollection('games');
  if (games === null) {
    console.log('create collection');  
    games = db.addCollection('games');
  }
    //coll.insert(game);
    server.listen(3000,function(){
        console.log('request!'); 
    });
}


10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.