Microsoft Azure PlayFab logo
    • Multiplayer
    • LiveOps
    • Data & Analytics
    • Add-ons
    • For Any Role

      • Engineer
      • Designer
      • Executive
      • Marketer
    • For Any Stage

      • Build
      • Improve
      • Grow
    • For Any Size

      • Solo
      • Indie
      • AAA
  • Runs on PlayFab
  • Pricing
    • Blog
    • Forums
    • Contact us
  • Sign up
  • Sign in
  • Ask a question
  • Spaces
    • PlayStream
    • Feature Requests
    • Add-on Marketplace
    • Bugs
    • API and SDK Questions
    • General Discussion
    • LiveOps
    • Topics
    • Questions
    • Articles
    • Ideas
    • Users
    • Badges
  • Home /
  • General Discussion /
avatar image
Question by lmgualandi · Oct 26, 2016 at 01:34 AM · Player DataShared Group Data

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?

Comment

People who like this

0 Show 0
10 |1200 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

2 Replies

· Add your reply
  • Sort: 
avatar image
Best Answer

Answer by Brendan · Oct 26, 2016 at 01:47 AM

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.

Comment

People who like this

0 Show 8 · Share
10 |1200 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image lmgualandi · Oct 27, 2016 at 04:11 PM 0
Share

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?

avatar image Brendan ♦♦ lmgualandi · Oct 27, 2016 at 04:13 PM 0
Share

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.

avatar image lmgualandi · Oct 28, 2016 at 09:13 PM 0
Share

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

avatar image Brendan ♦♦ lmgualandi · Oct 28, 2016 at 09:31 PM 0
Share

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).

avatar image katamaran · Jul 25, 2017 at 04:31 PM 0
Share

"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?

avatar image Brendan ♦♦ katamaran · Jul 25, 2017 at 04:33 PM 0
Share

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.

avatar image katamaran Brendan ♦♦ · Jul 25, 2017 at 07:34 PM 0
Share

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

Show more comments
avatar image

Answer by bda2206@gmail.com · Oct 30, 2016 at 12:42 AM

@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!'); 
    });
}


Comment

People who like this

0 Show 0 · Share
10 |1200 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Navigation

Spaces
  • General Discussion
  • API and SDK Questions
  • Feature Requests
  • PlayStream
  • Bugs
  • Add-on Marketplace
  • LiveOps
  • Follow this Question

    Answers Answers and Comments

    3 People are following this question.

    avatar image avatar image avatar image

    Related Questions

    Where to store photon gameStates. 1 Answer

    Bigger User Data (e.g. saving recordings) 1 Answer

    Should I use Playfab Party or Playfab Multiplayer Servers? 1 Answer

    Where/How to save players personal data? 1 Answer

    Questions on storing a list of strings on PlayFab to be accessed by all users, or similar methods 2 Answers

    PlayFab

    • Multiplayer
    • LiveOps
    • Data & Analytics
    • Runs on PlayFab
    • Pricing

    Solutions

    • For Any Role

      • Engineer
      • Designer
      • Executive
      • Marketer
    • For Any Stage

      • Build
      • Improve
      • Grow
    • For Any Size

      • Solo
      • Indie
      • AAA

    Engineers

    • Documentation
    • Quickstarts
    • API Reference
    • SDKs
    • Usage Limits

    Resources

    • Forums
    • Contact us
    • Blog
    • Service Health
    • Terms of Service
    • Attribution

    Follow us

    • Facebook
    • Twitter
    • LinkedIn
    • YouTube
    • Sitemap
    • Contact Microsoft
    • Privacy & cookies
    • Terms of use
    • Trademarks
    • Safety & eco
    • About our ads
    • © Microsoft 2020
    • Anonymous
    • Sign in
    • Create
    • Ask a question
    • Create an article
    • Post an idea
    • Spaces
    • PlayStream
    • Feature Requests
    • Add-on Marketplace
    • Bugs
    • API and SDK Questions
    • General Discussion
    • LiveOps
    • Explore
    • Topics
    • Questions
    • Articles
    • Ideas
    • Users
    • Badges