diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..e880677 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,3 @@ +{ + "phabricator.uri" : "https://phab.playfabdev.com/" +} \ No newline at end of file diff --git a/BasicSample/README.md b/BasicSample/README.md new file mode 100644 index 0000000..99c21d7 --- /dev/null +++ b/BasicSample/README.md @@ -0,0 +1,14 @@ +Cloud Script Basic Sample (basic_sample.js): +---- + +Please note that the Cloud Script automatically loaded as Revision 1 in all new titles is not specifically copied from this source. We do make an effort to keep them in sync, but if you do notice any differences, please feel free to let us know via the PlayFab forums. + +This file provides basic Cloud Script examples of: + + * helloWorld - Using the currentPlayerId (the logged-in user), output logging, and returning values + * completedLevel - Updating user statistics and user internal data (data which cannot be read or written by the client) + * updatePlayerMove, processPlayerMove - Calling a function from within Cloud Script, reading and updating user statistics, updating user internal data, basic server-side validation (checking that the reported value is within reason) + * RoomCreated, RoomJoined, RoomLeft, RoomClosed, RoomEventRaised - Handlers for managing webhook calls from a Photon Cloud server (see this document for more information: https://playfab.com/using-photon-playfab) + +For more information on using Cloud Script in PlayFab, please refer to our Example: + * https://github.com/PlayFab/SdkTestingCloudScript diff --git a/BasicSample/README.txt b/BasicSample/README.txt deleted file mode 100644 index 778543b..0000000 --- a/BasicSample/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -Cloud Script Basic Sample (basic_sample.js): ----------------------------------------------------------------------- -This file provides basic Cloud Script examples of: - - . helloWorld - Using the currentPlayerId (the logged-in user), output logging, and returning values - . completedLevel - Updating user statistics and user internal data (data which cannot be read or written by the client) - . updatePlayerMove, processPlayerMove - Calling a function from within Cloud Script, reading and updating user statistics, updating user internal data, basic server-side validation (checking that the reported value is within reason) - . RoomCreated, RoomJoined, RoomLeft, RoomClosed, RoomEventRaised - Handlers for managing webhook calls from a Photon Cloud server (see this document for more information: https://playfab.com/using-photon-playfab) - -For more information on using Cloud Script in PlayFab, please refer to this guide: - https://playfab.com/cloud-script diff --git a/BasicSample/basic_sample.js b/BasicSample/basic_sample.js index 552c141..23b4d48 100644 --- a/BasicSample/basic_sample.js +++ b/BasicSample/basic_sample.js @@ -1,47 +1,121 @@ /////////////////////////////////////////////////////////////////////////////////////////////////////// // -// Welcome to your first Cloud Script revision. -// The examples here provide a quick introduction to using Cloud Script and some -// ideas about how you might use it in your game. +// Welcome to your first Cloud Script revision! // -// There are two approaches for invoking Cloud Script: calling handler functions directly -// from the game client using the "RunCloudScript" API, or triggering Photon Webhooks associated with -// room events. Both approaches are demonstrated in this file. You can use one or the other, or both. +// Cloud Script runs in the PlayFab cloud and has full access to the PlayFab Game Server API +// (https://api.playfab.com/Documentation/Server), and it runs in the context of a securely +// authenticated player, so you can use it to implement logic for your game that is safe from +// client-side exploits. // -// Feel free to use this as a starting point for your game server logic, or to replace it altogether. -// If you have any questions or need advice on where to begin, -// check out the resources at https://playfab.com/cloud-script or check our forums at -// https://support.playfab.com. For issues which are confidential (involving sensitive intellectual -// property, for example), please contact our Developer Success team directly at devrel@playfab.com. +// Cloud Script functions can also make web requests to external HTTP +// endpoints, such as a database or private API for your title, which makes them a flexible +// way to integrate with your existing backend systems. // -// - The PlayFab Team +// There are several different options for calling Cloud Script functions: +// +// 1) Your game client calls them directly using the "ExecuteCloudScript" API, +// passing in the function name and arguments in the request and receiving the +// function return result in the response. +// (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) +// +// 2) You create PlayStream event actions that call them when a particular +// event occurs, passing in the event and associated player profile data. +// (https://api.playfab.com/playstream/docs) +// +// 3) For titles using the Photon Add-on (https://playfab.com/marketplace/photon/), +// Photon room events trigger webhooks which call corresponding Cloud Script functions. +// +// The following examples demonstrate all three options. // /////////////////////////////////////////////////////////////////////////////////////////////////////// -// This is a Cloud Script handler function. It runs in the PlayFab cloud and -// has full access to the PlayFab Game Server API -// (https://api.playfab.com/Documentation/Server). You can invoke the function -// from your game client by calling the "RunCloudScript" API -// (https://api.playfab.com/Documentation/Client/method/RunCloudScript) and -// specifying "helloWorld" for the "ActionId" field. -handlers.helloWorld = function (args) { - - // "currentPlayerId" is initialized to the PlayFab ID of the player logged-in on the game client. +// This is a Cloud Script function. "args" is set to the value of the "FunctionParameter" +// parameter of the ExecuteCloudScript API. +// (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) +// "context" contains additional information when the Cloud Script function is called from a PlayStream action. +handlers.helloWorld = function (args, context) { + + // The pre-defined "currentPlayerId" variable is initialized to the PlayFab ID of the player logged-in on the game client. // Cloud Script handles authenticating the player automatically. var message = "Hello " + currentPlayerId + "!"; - // You can use the "log" object to write out debugging statements. The "log" object has - // three functions corresponding to logging level: debug, info, and error. + // You can use the "log" object to write out debugging statements. It has + // three functions corresponding to logging level: debug, info, and error. These functions + // take a message string and an optional object. log.info(message); - - // Whatever value you return from a CloudScript handler function is passed back - // to the game client. It is set in the "Results" property of the object returned by the - // RunCloudScript API. Any log statments generated by the handler function are also included - // in the "ActionLog" field of the RunCloudScript result, so you can use them to assist in - // debugging and error handling. + var inputValue = null; + if (args && args.inputValue) + inputValue = args.inputValue; + log.debug("helloWorld:", { input: inputValue }); + + // The value you return from a Cloud Script function is passed back + // to the game client in the ExecuteCloudScript API response, along with any log statements + // and additional diagnostic information, such as any errors returned by API calls or external HTTP + // requests. They are also included in the optional player_executed_cloudscript PlayStream event + // generated by the function execution. + // (https://api.playfab.com/playstream/docs/PlayStreamEventModels/player/player_executed_cloudscript) return { messageValue: message }; -} +}; + +// This is a simple example of making a PlayFab server API call +handlers.makeAPICall = function (args, context) { + var request = { + PlayFabId: currentPlayerId, Statistics: [{ + StatisticName: "Level", + Value: 2 + }] + }; + // The pre-defined "server" object has functions corresponding to each PlayFab server API + // (https://api.playfab.com/Documentation/Server). It is automatically + // authenticated as your title and handles all communication with + // the PlayFab API, so you don't have to write extra code to issue HTTP requests. + var playerStatResult = server.UpdatePlayerStatistics(request); +}; + +// This is a simple example of making a web request to an external HTTP API. +handlers.makeHTTPRequest = function (args, context) { + var headers = { + "X-MyCustomHeader": "Some Value" + }; + + var body = { + input: args, + userId: currentPlayerId, + mode: "foobar" + }; + + var url = "http://httpbin.org/status/200"; + var content = JSON.stringify(body); + var httpMethod = "post"; + var contentType = "application/json"; + + // The pre-defined http object makes synchronous HTTP requests + var response = http.request(url, httpMethod, content, contentType, headers); + return { responseContent: response }; +}; + +// This is a simple example of a function that is called from a +// PlayStream event action. (https://playfab.com/introducing-playstream/) +handlers.handlePlayStreamEventAndProfile = function (args, context) { + + // The event that triggered the action + // (https://api.playfab.com/playstream/docs/PlayStreamEventModels) + var psEvent = context.playStreamEvent; + + // The profile data of the player associated with the event + // (https://api.playfab.com/playstream/docs/PlayStreamProfileModels) + var profile = context.playerProfile; + + // Post data about the event to an external API + var content = JSON.stringify({ user: profile.PlayerId, event: psEvent.EventName }); + var response = http.request('https://httpbin.org/status/200', 'post', content, 'application/json', null); + + return { externalAPIResponse: response }; +}; + + +// Below are some examples of using Cloud Script in slightly more realistic scenarios // This is a function that the game client would call whenever a player completes // a level. It updates a setting in the player's data that only game server @@ -51,19 +125,10 @@ handlers.helloWorld = function (args) { // A funtion like this could be extended to perform validation on the // level completion data to detect cheating. It could also do things like // award the player items from the game catalog based on their performance. -handlers.completedLevel = function (args) { - - // "args" is set to the value of the "Params" field of the object passed in to - // RunCloudScript from the client. It contains whatever properties you want to pass - // into your Cloud Script function. In this case it contains information about - // the level a player has completed. +handlers.completedLevel = function (args, context) { var level = args.levelName; var monstersKilled = args.monstersKilled; - - // The "server" object has functions for each PlayFab server API - // (https://api.playfab.com/Documentation/Server). It is automatically - // authenticated as your title and handles all communication with - // the PlayFab API, so you don't have to write the code to make web requests. + var updateUserDataResult = server.UpdateUserInternalData({ PlayFabId: currentPlayerId, Data: { @@ -72,16 +137,15 @@ handlers.completedLevel = function (args) { }); log.debug("Set lastLevelCompleted for player " + currentPlayerId + " to " + level); - - server.UpdateUserStatistics({ - PlayFabId: currentPlayerId, - UserStatistics: { - level_monster_kills: monstersKilled - } - }); - + var request = { + PlayFabId: currentPlayerId, Statistics: [{ + StatisticName: "level_monster_kills", + Value: monstersKilled + }] + }; + server.UpdatePlayerStatistics(request); log.debug("Updated level_monster_kills stat for player " + currentPlayerId + " to " + monstersKilled); -} +}; // In addition to the Cloud Script handlers, you can define your own functions and call them from your handlers. @@ -89,7 +153,7 @@ handlers.completedLevel = function (args) { handlers.updatePlayerMove = function (args) { var validMove = processPlayerMove(args); return { validMove: validMove }; -} +}; // This is a helper function that verifies that the player's move wasn't made @@ -97,10 +161,12 @@ handlers.updatePlayerMove = function (args) { // If the move is valid, then it updates the player's statistics and profile data. // This function is called from the "UpdatePlayerMove" handler above and also is // triggered by the "RoomEventRaised" Photon room event in the Webhook handler -// below. For this example, the script defines the cooldown period (playerMoveCooldownInSeconds) +// below. +// +// For this example, the script defines the cooldown period (playerMoveCooldownInSeconds) // as 15 seconds. A recommended approach for values like this would be to create them in Title -// Data, so that they can be queries in the script with a call to -// https://api.playfab.com/Documentation/Server/method/GetTitleData. This would allow you to +// Data, so that they can be queries in the script with a call to GetTitleData +// (https://api.playfab.com/Documentation/Server/method/GetTitleData). This would allow you to // make adjustments to these values over time, without having to edit, test, and roll out an // updated script. function processPlayerMove(playerMove) { @@ -120,86 +186,189 @@ function processPlayerMove(playerMove) { log.debug("lastMoveTime: " + lastMoveTime + " now: " + now + " timeSinceLastMoveInSeconds: " + timeSinceLastMoveInSeconds); if (timeSinceLastMoveInSeconds < playerMoveCooldownInSeconds) { - log.error("Invalid move - time since last move: " + timeSinceLastMoveInSeconds + "s less than minimum of " + playerMoveCooldownInSeconds + "s.") + log.error("Invalid move - time since last move: " + timeSinceLastMoveInSeconds + "s less than minimum of " + playerMoveCooldownInSeconds + "s."); return false; } } - var playerStats = server.GetUserStatistics({ + var playerStats = server.GetPlayerStatistics({ PlayFabId: currentPlayerId - }).UserStatistics; - - if (playerStats.movesMade) - playerStats.movesMade += 1; - else - playerStats.movesMade = 1; - - server.UpdateUserStatistics({ - PlayFabId: currentPlayerId, - UserStatistics: playerStats - }); - + }).Statistics; + var movesMade = 0; + for (var i = 0; i < playerStats.length; i++) + if (playerStats[i].StatisticName === "") + movesMade = playerStats[i].Value; + movesMade += 1; + var request = { + PlayFabId: currentPlayerId, Statistics: [{ + StatisticName: "movesMade", + Value: movesMade + }] + }; + server.UpdatePlayerStatistics(request); server.UpdateUserInternalData({ PlayFabId: currentPlayerId, Data: { - last_move_timestamp: new Date(now).toUTCString() + last_move_timestamp: new Date(now).toUTCString(), + last_move: JSON.stringify(playerMove) } }); return true; } - +// This is an example of using PlayStream real-time segmentation to trigger +// game logic based on player behavior. (https://playfab.com/introducing-playstream/) +// The function is called when a player_statistic_changed PlayStream event causes a player +// to enter a segment defined for high skill players. It sets a key value in +// the player's internal data which unlocks some new content for the player. +handlers.unlockHighSkillContent = function (args, context) { + var playerStatUpdatedEvent = context.playStreamEvent; + var request = { + PlayFabId: currentPlayerId, + Data: { + "HighSkillContent": "true", + "XPAtHighSkillUnlock": playerStatUpdatedEvent.StatisticValue.toString() + } + }; + var playerInternalData = server.UpdateUserInternalData(request); + log.info('Unlocked HighSkillContent for ' + context.playerProfile.DisplayName); + return { profile: context.playerProfile }; +}; // Photon Webhooks Integration // -// Note (April 2015): This is feature is currently in limited access Beta. To request early -// access please email beta@playfab.com. -// // The following functions are examples of Photon Cloud Webhook handlers. -// When you enable Photon integration in the Game Manager, your Photon applications -// are automatically configured to authenticate players using their PlayFab accounts -// and to fire events that trigger your CloudScript Webhook handlers, if defined. -// This makes it easier than ever to incorporate server logic into your game. -// -// For more information, see https://playfab.com/using-photon-playfab +// When you enable the Photon Add-on (https://playfab.com/marketplace/photon/) +// in the Game Manager, your Photon applications are automatically configured +// to authenticate players using their PlayFab accounts and to fire events that +// trigger your Cloud Script Webhook handlers, if defined. +// This makes it easier than ever to incorporate multiplayer server logic into your game. // Triggered automatically when a Photon room is first created handlers.RoomCreated = function (args) { - log.debug("Room Created - Game: " + args.GameId + " MaxPlayers: " + args.CreateOptions.MaxPlayers); -} + server.WritePlayerEvent({ + EventName : "room_created", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + Type: args.Type, + ActorNr: args.ActorNr, + CreateOptions: args.CreateOptions + } + } + }); +}; // Triggered automatically when a player joins a Photon room handlers.RoomJoined = function (args) { - log.debug("Room Joined - Game: " + args.GameId + " PlayFabId: " + args.UserId); -} + server.WritePlayerEvent({ + EventName: "room_joined", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + ActorNr: args.ActorNr + } + } + }); +}; // Triggered automatically when a player leaves a Photon room handlers.RoomLeft = function (args) { - log.debug("Room Left - Game: " + args.GameId + " PlayFabId: " + args.UserId); -} + server.WritePlayerEvent({ + EventName: "room_left", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + Type: args.Type, + ActorNr: args.ActorNr, + IsInactive: args.IsInactive + } + } + }); +}; // Triggered automatically when a Photon room closes // Note: currentPlayerId is undefined in this function handlers.RoomClosed = function (args) { - log.debug("Room Closed - Game: " + args.GameId); -} + server.WriteTitleEvent({ + EventName: "room_closed", + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + Type: args.Type, + ActorCount: args.ActorCount + } + } + }); +}; // Triggered automatically when a Photon room game property is updated. -// Note: currentPlayerId is undefined in this function -handlers.RoomPropertyUpdated = function(args) { - log.debug("Room Property Updated - Game: " + args.GameId); -} +handlers.RoomPropertyUpdated = function (args) { + if (args.Type === "Game") { + server.WritePlayerEvent({ + EventName: "room_properties_updated", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + ActorNr: args.ActorNr, + Properties: args.Properties + } + } + }); + } else { // "Actor" + server.WritePlayerEvent({ + EventName: "player_roperties_updated", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + ActorNr: args.ActorNr, + TargetActor: args.TargetActor, + Properties: args.Properties + } + } + }); + } +}; // Triggered by calling "OpRaiseEvent" on the Photon client. The "args.Data" property is // set to the value of the "customEventContent" HashTable parameter, so you can use // it to pass in arbitrary data. handlers.RoomEventRaised = function (args) { - var eventData = args.Data; - log.debug("Event Raised - Game: " + args.GameId + " Event Type: " + eventData.eventType); - - switch (eventData.eventType) { + server.WritePlayerEvent({ + EventName: "event_raised", + PlayFabId: args.UserId, + Body: { + WebHook: { + AppVersion: args.AppVersion, + Region: args.Region, + GameId: args.GameId, + ActorNr: args.ActorNr, + EvCode: args.EvCode + } + } + }); + + var eventData = args.Data; + switch (eventData.eventType) { // use args.EvCode instead of embedding eventType in payload case "playerMove": processPlayerMove(eventData); break; @@ -207,4 +376,4 @@ handlers.RoomEventRaised = function (args) { default: break; } -} +}; diff --git a/GetPlayersInSegmentSample/README.md b/GetPlayersInSegmentSample/README.md new file mode 100644 index 0000000..83f5bdd --- /dev/null +++ b/GetPlayersInSegmentSample/README.md @@ -0,0 +1,8 @@ +## Samples for GetPlayersInSegment API usage in CloudScript: + +#### API doc: [Get Players In Segment](https://learn.microsoft.com/en-us/rest/api/playfab/server/play-stream/get-players-in-segment?view=playfab-rest) + +#### The file get_players_in_segment_api_sample.js provides examples of: + +- GetPlayersInSegmentSample: Using the GetPlayersInSegment API to get all the player profiles in a Segment and do some processing on the profiles +- GetSegmentPlayerCountSample: Using the GetPlayersInSegment API to get the count of player profiles in a Segment \ No newline at end of file diff --git a/GetPlayersInSegmentSample/get_players_in_segment_api_sample.js b/GetPlayersInSegmentSample/get_players_in_segment_api_sample.js new file mode 100644 index 0000000..4dcce79 --- /dev/null +++ b/GetPlayersInSegmentSample/get_players_in_segment_api_sample.js @@ -0,0 +1,64 @@ +handlers.GetPlayersInSegmentSample = function (args, context) { + + /* + Sample code to use the GetPlayersInSegment API to process the player profiles in a Segment. + The GetPlayersInSegment API pages through the all the player profiles + in the Segment in batches of size 'MaxBatchSize'. + API Doc: https://learn.microsoft.com/en-us/rest/api/playfab/server/play-stream/get-players-in-segment?view=playfab-rest + */ + + var request = { + GetProfilesAsync: true, // setting to 'true' is highly recommended to avoid network timeouts + MaxBatchSize: 1000, // 1000 is the default value. Maximum is 10,000 + SegmentId: "AAAAAAAAA" // provide your SegmentId here OR you can add SegmentId in the JSON args sent from PlayFab caller/game client and use that + } + + // make the first GetPlayersInSegment API call + var playersInSegmentResult = server.GetPlayersInSegment(request); + + // process until continuation token is not null to get all the profiles in this Segment + while (playersInSegmentResult.ContinuationToken != null) + { + // get the current batch of player profiles + var playerProfiles = playersInSegmentResult.PlayerProfiles; + + if (playerProfiles && playerProfiles.length > 0) + { + for(let i=0;i