├── .gitignore ├── CloudScriptExample.njsproj ├── CloudScriptExample.sln ├── CloudScriptJobTemplate.yml ├── CloudScriptResourceTemplate.yml ├── CloudScriptStepTemplate.yml ├── DefaultCloudScript.js ├── DefaultCloudScript.ts ├── ExampleCloudScript.js ├── ExampleCloudScript.ts ├── LICENSE ├── LiveOpsExample.ts ├── OtherExamples.ts ├── README.md ├── Scripts ├── Entity.ts └── typings │ └── PlayFab │ ├── CloudScript.d.ts │ └── PlayStream.d.ts ├── TitleAB.ts ├── UnicornBattle.js ├── UnicornBattle.ts ├── genConfig.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/*[Tt]estTitleData.json* 2 | 3 | .vs/ 4 | bin/ 5 | obj/ 6 | 7 | .ntvs_analysis.dat 8 | *.js 9 | *.map 10 | *.user 11 | -------------------------------------------------------------------------------- /CloudScriptExample.njsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 11.0 5 | C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\Microsoft\VisualStudio\v15.0 6 | C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\Microsoft\VisualStudio\v15.0 7 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 8 | CloudScriptExample 9 | CloudScriptExample 10 | 11 | 12 | 13 | Debug 14 | 2.0 15 | 19af8a73-bf95-4f77-828e-2e1c33de86c4 16 | . 17 | ExampleCloudScript.ts 18 | False 19 | 20 | 21 | . 22 | . 23 | v4.0 24 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 25 | ShowAllFiles 26 | true 27 | CommonJS 28 | true 29 | false 30 | 31 | 32 | true 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Code 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | False 65 | True 66 | 0 67 | / 68 | http://localhost:48022/ 69 | False 70 | True 71 | http://localhost:1337 72 | False 73 | 74 | 75 | 76 | 77 | 78 | 79 | CurrentPage 80 | True 81 | False 82 | False 83 | False 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | False 93 | False 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /CloudScriptExample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "CloudScriptExample", "CloudScriptExample.njsproj", "{19AF8A73-BF95-4F77-828E-2E1C33DE86C4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {19AF8A73-BF95-4F77-828E-2E1C33DE86C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {19AF8A73-BF95-4F77-828E-2E1C33DE86C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {19AF8A73-BF95-4F77-828E-2E1C33DE86C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {19AF8A73-BF95-4F77-828E-2E1C33DE86C4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /CloudScriptJobTemplate.yml: -------------------------------------------------------------------------------- 1 | # JOB LEVEL TEMPLATE: 2 | # Used to build CloudScriptSamples 3 | # Reusable 4 | # Meant to be run from the single CloudScriptTemplate pipeline (default), or 5 | # from a multi-pipeline such as publishing (should specify alternate params) 6 | 7 | parameters: 8 | - name: ApiSpecSource 9 | displayName: ApiSpecSource 10 | type: string 11 | default: -apiSpecGitUrl https://raw.githubusercontent.com/PlayFab/API_Specs/master/ 12 | - name: CommitMessage 13 | displayName: CommitMessage 14 | type: string 15 | default: Automated build from ADO Pipeline 16 | - name: GitDestBranch 17 | displayName: GitDestBranch 18 | type: string 19 | default: doNotCommit 20 | - name: SelfTemplateResource 21 | displayName: SelfTemplateResource 22 | type: string 23 | default: self 24 | 25 | jobs: 26 | - job: CloudScriptJobTemplate 27 | steps: 28 | - bash: echo CloudScriptJobTemplate 29 | - job: CloudScriptTemplate 30 | pool: 31 | vmImage: 'windows-latest' 32 | steps: 33 | - template: CloudScriptStepTemplate.yml 34 | parameters: 35 | ApiSpecSource: ${{ parameters.ApiSpecSource }} 36 | CommitMessage: ${{ parameters.CommitMessage }} 37 | GitDestBranch: ${{ parameters.GitDestBranch }} 38 | SelfTemplateResource: ${{ parameters.SelfTemplateResource }} 39 | -------------------------------------------------------------------------------- /CloudScriptResourceTemplate.yml: -------------------------------------------------------------------------------- 1 | # TOP LEVEL TEMPLATE: 2 | # Can only be used to run the single CloudScriptTemplate pipeline. 3 | # Why: Resources can only be defined once. 4 | # This determines the resources available to all "jobs" and "steps" no matter which templates are loaded after this 5 | # If resources is ever defined again, it'll break so badly that the pipeline won't even parse 6 | 7 | resources: 8 | repositories: 9 | - repository: JenkinsSdkSetupScripts 10 | type: github 11 | endpoint: GitHub_PlayFab 12 | name: PlayFab/JenkinsSdkSetupScripts 13 | - repository: API_Specs 14 | type: github 15 | endpoint: GitHub_PlayFab 16 | name: PlayFab/API_Specs 17 | - repository: SdkGenerator 18 | type: github 19 | endpoint: GitHub_PlayFab 20 | name: PlayFab/SdkGenerator 21 | - repository: SDKTestingCloudscript 22 | endpoint: GitHub_PlayFab 23 | type: github 24 | name: PlayFab/SDKTestingCloudscript 25 | 26 | jobs: 27 | - job: CloudScriptResourceTemplate 28 | steps: 29 | - bash: echo CloudScriptResourceTemplate 30 | - template: CloudScriptJobTemplate.yml 31 | -------------------------------------------------------------------------------- /CloudScriptStepTemplate.yml: -------------------------------------------------------------------------------- 1 | # STEPS LEVEL TEMPLATE: 2 | # Used to build CloudScript 3 | # Reusable 4 | # Used to "hide" the additional variables specific to this SDK which shouldn't be set from a higher level, or 5 | # shared from a multi-build pipeline like a publish 6 | 7 | parameters: 8 | - name: ApiSpecSource 9 | displayName: ApiSpecSource 10 | type: string 11 | default: -apiSpecGitUrl https://raw.githubusercontent.com/PlayFab/API_Specs/master/ 12 | - name: CommitMessage 13 | displayName: CommitMessage 14 | type: string 15 | default: Automated build from ADO Pipeline 16 | - name: GitDestBranch 17 | displayName: GitDestBranch 18 | type: string 19 | default: doNotCommit 20 | - name: SdkName 21 | displayName: SdkName 22 | type: string 23 | default: SDKTestingCloudscript 24 | - name: SelfTemplateResource 25 | displayName: SelfTemplateResource 26 | type: string 27 | default: self 28 | 29 | steps: 30 | - checkout: JenkinsSdkSetupScripts 31 | clean: true 32 | path: s 33 | - checkout: API_Specs 34 | clean: true 35 | path: s/API_Specs 36 | - checkout: SdkGenerator 37 | clean: true 38 | path: s/SdkGenerator 39 | - checkout: ${{ parameters.SelfTemplateResource }} 40 | clean: true 41 | submodules: true 42 | path: s/sdks/SdkTestingCloudScript 43 | persistCredentials: true 44 | - bash: | 45 | set -e 46 | 47 | echo alias the ADO variables into local variables 48 | ApiSpecSource="${{ parameters.ApiSpecSource }}" 49 | CommitMessage="${{ parameters.CommitMessage }}" 50 | GitDestBranch="${{ parameters.GitDestBranch }}" 51 | SdkName="${{ parameters.SdkName }}" 52 | WORKSPACE=$(pwd) 53 | # Hack attempt to get WORKSPACE into a sub-environment 54 | export WORKSPACE="$WORKSPACE" 55 | 56 | . "$WORKSPACE/JenkinsSdkSetupScripts/JenkinsScripts/Pipeline/util.sh" 57 | . "$WORKSPACE/JenkinsSdkSetupScripts/JenkinsScripts/Pipeline/testInit.sh" 58 | 59 | cd "$WORKSPACE/SDKGenerator/SDKBuildScripts" 60 | . ./shared_build.sh 61 | 62 | echo === Build the CloudScript Project === 63 | Find2019MsBuild || Find2017MsBuild 64 | cd $WORKSPACE/sdks/$SdkName/ 65 | "$MSBUILD_EXE" CloudScriptExample.sln 66 | 67 | . "$WORKSPACE/SDKGenerator/JenkinsConsoleUtility/JenkinsScripts/gitFinalize.sh" 68 | 69 | displayName: 'Build/Test/Report' 70 | 71 | -------------------------------------------------------------------------------- /DefaultCloudScript.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Welcome to your first Cloud Script revision! 4 | // 5 | // Cloud Script runs in the PlayFab cloud and has full access to the PlayFab Game Server API 6 | // (https://api.playfab.com/Documentation/Server), and it runs in the context of a securely 7 | // authenticated player, so you can use it to implement logic for your game that is safe from 8 | // client-side exploits. 9 | // 10 | // Cloud Script functions can also make web requests to external HTTP 11 | // endpoints, such as a database or private API for your title, which makes them a flexible 12 | // way to integrate with your existing backend systems. 13 | // 14 | // There are several different options for calling Cloud Script functions: 15 | // 16 | // 1) Your game client calls them directly using the "ExecuteCloudScript" API, 17 | // passing in the function name and arguments in the request and receiving the 18 | // function return result in the response. 19 | // (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) 20 | // 21 | // 2) You create PlayStream event actions that call them when a particular 22 | // event occurs, passing in the event and associated player profile data. 23 | // (https://api.playfab.com/playstream/docs) 24 | // 25 | // 3) For titles using the Photon Add-on (https://playfab.com/marketplace/photon/), 26 | // Photon room events trigger webhooks which call corresponding Cloud Script functions. 27 | // 28 | // The following examples demonstrate all three options. 29 | // 30 | /////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | // This is a Cloud Script function. "args" is set to the value of the "FunctionParameter" 32 | // parameter of the ExecuteCloudScript API. 33 | // (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) 34 | // "context" contains additional information when the Cloud Script function is called from a PlayStream action. 35 | var HelloWorldDefault = function (args, context) { 36 | // The pre-defined "currentPlayerId" variable is initialized to the PlayFab ID of the player logged-in on the game client. 37 | // Cloud Script handles authenticating the player automatically. 38 | var message = "Hello " + currentPlayerId + "!"; 39 | // You can use the "log" object to write out debugging statements. It has 40 | // three functions corresponding to logging level: debug, info, and error. These functions 41 | // take a message string and an optional object. 42 | log.info(message); 43 | var inputValue = null; 44 | if (args && args.inputValue) 45 | inputValue = args.inputValue; 46 | log.debug("helloWorld:", { input: args.inputValue }); 47 | // The value you return from a Cloud Script function is passed back 48 | // to the game client in the ExecuteCloudScript API response, along with any log statements 49 | // and additional diagnostic information, such as any errors returned by API calls or external HTTP 50 | // requests. They are also included in the optional player_executed_cloudscript PlayStream event 51 | // generated by the function execution. 52 | // (https://api.playfab.com/playstream/docs/PlayStreamEventModels/player/player_executed_cloudscript) 53 | return { messageValue: message }; 54 | }; 55 | handlers["helloWorld"] = HelloWorldDefault; 56 | // This is a simple example of making a PlayFab server API call 57 | var MakeApiCall = function (args, context) { 58 | var request = { 59 | PlayFabId: currentPlayerId, Statistics: [{ 60 | StatisticName: "Level", 61 | Value: 2 62 | }] 63 | }; 64 | // The pre-defined "server" object has functions corresponding to each PlayFab server API 65 | // (https://api.playfab.com/Documentation/Server). It is automatically 66 | // authenticated as your title and handles all communication with 67 | // the PlayFab API, so you don't have to write extra code to issue HTTP requests. 68 | var playerStatResult = server.UpdatePlayerStatistics(request); 69 | }; 70 | handlers["makeAPICall"] = MakeApiCall; 71 | // This is a simple example of making a web request to an external HTTP API. 72 | var MakeHttpRequest = function (args, context) { 73 | var headers = { 74 | "X-MyCustomHeader": "Some Value" 75 | }; 76 | var body = { 77 | input: args, 78 | userId: currentPlayerId, 79 | mode: "foobar" 80 | }; 81 | var url = "http://httpbin.org/status/200"; 82 | var content = JSON.stringify(body); 83 | var httpMethod = "post"; 84 | var contentType = "application/json"; 85 | // The pre-defined http object makes synchronous HTTP requests 86 | var response = http.request(url, httpMethod, content, contentType, headers); 87 | return { responseContent: response }; 88 | }; 89 | handlers["makeHTTPRequest"] = MakeHttpRequest; 90 | // This is a simple example of a function that is called from a 91 | // PlayStream event action. (https://playfab.com/introducing-playstream/) 92 | var HandlePlayStreamEventAndProfile = function (args, context) { 93 | // The event that triggered the action 94 | // (https://api.playfab.com/playstream/docs/PlayStreamEventModels) 95 | var psEvent = context.playStreamEvent; 96 | // The profile data of the player associated with the event 97 | // (https://api.playfab.com/playstream/docs/PlayStreamProfileModels) 98 | var profile = context.playerProfile; 99 | // Post data about the event to an external API 100 | var content = JSON.stringify({ user: profile.PlayerId, event: psEvent.EventName }); 101 | var response = http.request('https://httpbin.org/status/200', 'post', content, 'application/json', null); 102 | return { externalAPIResponse: response }; 103 | }; 104 | handlers["handlePlayStreamEventAndProfile"] = HandlePlayStreamEventAndProfile; 105 | // Below are some examples of using Cloud Script in slightly more realistic scenarios 106 | // This is a function that the game client would call whenever a player completes 107 | // a level. It updates a setting in the player's data that only game server 108 | // code can write - it is read-only on the client - and it updates a player 109 | // statistic that can be used for leaderboards. 110 | // 111 | // A funtion like this could be extended to perform validation on the 112 | // level completion data to detect cheating. It could also do things like 113 | // award the player items from the game catalog based on their performance. 114 | var CompletedLevel = function (args, context) { 115 | var level = args.levelName; 116 | var monstersKilled = args.monstersKilled; 117 | var updateUserDataResult = server.UpdateUserInternalData({ 118 | PlayFabId: currentPlayerId, 119 | Data: { 120 | lastLevelCompleted: level 121 | } 122 | }); 123 | log.debug("Set lastLevelCompleted for player " + currentPlayerId + " to " + level); 124 | var request = { 125 | PlayFabId: currentPlayerId, Statistics: [{ 126 | StatisticName: "level_monster_kills", 127 | Value: monstersKilled 128 | }] 129 | }; 130 | server.UpdatePlayerStatistics(request); 131 | log.debug("Updated level_monster_kills stat for player " + currentPlayerId + " to " + monstersKilled); 132 | }; 133 | handlers["completedLevel"] = CompletedLevel; 134 | // In addition to the Cloud Script handlers, you can define your own functions and call them from your handlers. 135 | // This makes it possible to share code between multiple handlers and to improve code organization. 136 | var UpdatePlayerMove = function (args) { 137 | var validMove = processPlayerMove(args); 138 | return { validMove: validMove }; 139 | }; 140 | handlers["updatePlayerMove"] = UpdatePlayerMove; 141 | // This is a helper function that verifies that the player's move wasn't made 142 | // too quickly following their previous move, according to the rules of the game. 143 | // If the move is valid, then it updates the player's statistics and profile data. 144 | // This function is called from the "UpdatePlayerMove" handler above and also is 145 | // triggered by the "RoomEventRaised" Photon room event in the Webhook handler 146 | // below. 147 | // 148 | // For this example, the script defines the cooldown period (playerMoveCooldownInSeconds) 149 | // as 15 seconds. A recommended approach for values like this would be to create them in Title 150 | // Data, so that they can be queries in the script with a call to GetTitleData 151 | // (https://api.playfab.com/Documentation/Server/method/GetTitleData). This would allow you to 152 | // make adjustments to these values over time, without having to edit, test, and roll out an 153 | // updated script. 154 | function processPlayerMove(playerMove) { 155 | var now = Date.now(); 156 | var playerMoveCooldownInSeconds = 15; 157 | var playerData = server.GetUserInternalData({ 158 | PlayFabId: currentPlayerId, 159 | Keys: ["last_move_timestamp"] 160 | }); 161 | var lastMoveTimestampSetting = playerData.Data["last_move_timestamp"]; 162 | if (lastMoveTimestampSetting) { 163 | var lastMoveTime = Date.parse(lastMoveTimestampSetting.Value); 164 | var timeSinceLastMoveInSeconds = (now - lastMoveTime) / 1000; 165 | log.debug("lastMoveTime: " + lastMoveTime + " now: " + now + " timeSinceLastMoveInSeconds: " + timeSinceLastMoveInSeconds); 166 | if (timeSinceLastMoveInSeconds < playerMoveCooldownInSeconds) { 167 | log.error("Invalid move - time since last move: " + timeSinceLastMoveInSeconds + "s less than minimum of " + playerMoveCooldownInSeconds + "s."); 168 | return false; 169 | } 170 | } 171 | var playerStats = server.GetPlayerStatistics({ 172 | PlayFabId: currentPlayerId 173 | }).Statistics; 174 | var movesMade = 0; 175 | for (var i = 0; i < playerStats.length; i++) 176 | if (playerStats[i].StatisticName === "") 177 | movesMade = playerStats[i].Value; 178 | movesMade += 1; 179 | var request = { 180 | PlayFabId: currentPlayerId, Statistics: [{ 181 | StatisticName: "movesMade", 182 | Value: movesMade 183 | }] 184 | }; 185 | server.UpdatePlayerStatistics(request); 186 | server.UpdateUserInternalData({ 187 | PlayFabId: currentPlayerId, 188 | Data: { 189 | last_move_timestamp: new Date(now).toUTCString(), 190 | last_move: JSON.stringify(playerMove) 191 | } 192 | }); 193 | return true; 194 | } 195 | // This is an example of using PlayStream real-time segmentation to trigger 196 | // game logic based on player behavior. (https://playfab.com/introducing-playstream/) 197 | // The function is called when a player_statistic_changed PlayStream event causes a player 198 | // to enter a segment defined for high skill players. It sets a key value in 199 | // the player's internal data which unlocks some new content for the player. 200 | var UnlockHighSkillContent = function (args, context) { 201 | var playerStatUpdatedEvent = context.playStreamEvent; 202 | var request = { 203 | PlayFabId: currentPlayerId, 204 | Data: { 205 | "HighSkillContent": "true", 206 | "XPAtHighSkillUnlock": playerStatUpdatedEvent.StatisticValue.toString() 207 | } 208 | }; 209 | var playerInternalData = server.UpdateUserInternalData(request); 210 | log.info('Unlocked HighSkillContent for ' + context.playerProfile.DisplayName); 211 | return { profile: context.playerProfile }; 212 | }; 213 | handlers["unlockHighSkillContent"] = UnlockHighSkillContent; 214 | // Photon Webhooks Integration 215 | // 216 | // The following functions are examples of Photon Cloud Webhook handlers. 217 | // When you enable the Photon Add-on (https://playfab.com/marketplace/photon/) 218 | // in the Game Manager, your Photon applications are automatically configured 219 | // to authenticate players using their PlayFab accounts and to fire events that 220 | // trigger your Cloud Script Webhook handlers, if defined. 221 | // This makes it easier than ever to incorporate multiplayer server logic into your game. 222 | // Triggered automatically when a Photon room is first created 223 | var RoomCreated = function (args) { 224 | log.debug("Room Created - Game: " + args.GameId + " MaxPlayers: " + args.CreateOptions.MaxPlayers); 225 | }; 226 | handlers["RoomCreated"] = RoomCreated; 227 | // Triggered automatically when a player joins a Photon room 228 | var RoomJoined = function (args) { 229 | log.debug("Room Joined - Game: " + args.GameId + " PlayFabId: " + args.UserId); 230 | }; 231 | handlers["RoomJoined"] = RoomJoined; 232 | // Triggered automatically when a player leaves a Photon room 233 | var RoomLeft = function (args) { 234 | log.debug("Room Left - Game: " + args.GameId + " PlayFabId: " + args.UserId); 235 | }; 236 | handlers["RoomLeft"] = RoomLeft; 237 | // Triggered automatically when a Photon room closes 238 | // Note: currentPlayerId is undefined in this function 239 | var RoomClosed = function (args) { 240 | log.debug("Room Closed - Game: " + args.GameId); 241 | }; 242 | handlers["RoomClosed"] = RoomClosed; 243 | // Triggered automatically when a Photon room game property is updated. 244 | // Note: currentPlayerId is undefined in this function 245 | var RoomPropertyUpdated = function (args) { 246 | log.debug("Room Property Updated - Game: " + args.GameId); 247 | }; 248 | handlers["RoomPropertyUpdated"] = RoomPropertyUpdated; 249 | // Triggered by calling "OpRaiseEvent" on the Photon client. The "args.Data" property is 250 | // set to the value of the "customEventContent" HashTable parameter, so you can use 251 | // it to pass in arbitrary data. 252 | var RoomEventRaised = function (args) { 253 | var eventData = args.Data; 254 | log.debug("Event Raised - Game: " + args.GameId + " Event Type: " + eventData.eventType); 255 | switch (eventData.eventType) { 256 | case "playerMove": 257 | processPlayerMove(eventData); 258 | break; 259 | default: 260 | break; 261 | } 262 | }; 263 | handlers["RoomEventRaised"] = RoomEventRaised; 264 | //# sourceMappingURL=DefaultCloudScript.js.map -------------------------------------------------------------------------------- /DefaultCloudScript.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Welcome to your first Cloud Script revision! 4 | // 5 | // Cloud Script runs in the PlayFab cloud and has full access to the PlayFab Game Server API 6 | // (https://api.playfab.com/Documentation/Server), and it runs in the context of a securely 7 | // authenticated player, so you can use it to implement logic for your game that is safe from 8 | // client-side exploits. 9 | // 10 | // Cloud Script functions can also make web requests to external HTTP 11 | // endpoints, such as a database or private API for your title, which makes them a flexible 12 | // way to integrate with your existing backend systems. 13 | // 14 | // There are several different options for calling Cloud Script functions: 15 | // 16 | // 1) Your game client calls them directly using the "ExecuteCloudScript" API, 17 | // passing in the function name and arguments in the request and receiving the 18 | // function return result in the response. 19 | // (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) 20 | // 21 | // 2) You create PlayStream event actions that call them when a particular 22 | // event occurs, passing in the event and associated player profile data. 23 | // (https://api.playfab.com/playstream/docs) 24 | // 25 | // 3) For titles using the Photon Add-on (https://playfab.com/marketplace/photon/), 26 | // Photon room events trigger webhooks which call corresponding Cloud Script functions. 27 | // 28 | // The following examples demonstrate all three options. 29 | // 30 | /////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | // This is a Cloud Script function. "args" is set to the value of the "FunctionParameter" 33 | // parameter of the ExecuteCloudScript API. 34 | // (https://api.playfab.com/Documentation/Client/method/ExecuteCloudScript) 35 | // "context" contains additional information when the Cloud Script function is called from a PlayStream action. 36 | var HelloWorldDefault = function (args: any, context: IPlayFabContext): IHelloWorldResponse { 37 | 38 | // The pre-defined "currentPlayerId" variable is initialized to the PlayFab ID of the player logged-in on the game client. 39 | // Cloud Script handles authenticating the player automatically. 40 | var message = "Hello " + currentPlayerId + "!"; 41 | 42 | // You can use the "log" object to write out debugging statements. It has 43 | // three functions corresponding to logging level: debug, info, and error. These functions 44 | // take a message string and an optional object. 45 | log.info(message); 46 | var inputValue = null; 47 | if (args && args.inputValue) 48 | inputValue = args.inputValue; 49 | log.debug("helloWorld:", { input: args.inputValue }); 50 | 51 | // The value you return from a Cloud Script function is passed back 52 | // to the game client in the ExecuteCloudScript API response, along with any log statements 53 | // and additional diagnostic information, such as any errors returned by API calls or external HTTP 54 | // requests. They are also included in the optional player_executed_cloudscript PlayStream event 55 | // generated by the function execution. 56 | // (https://api.playfab.com/playstream/docs/PlayStreamEventModels/player/player_executed_cloudscript) 57 | return { messageValue: message }; 58 | } 59 | interface IHelloWorldResponse { 60 | messageValue: string; 61 | } 62 | handlers["helloWorld"] = HelloWorldDefault; 63 | 64 | // This is a simple example of making a PlayFab server API call 65 | var MakeApiCall = function (args: any, context: IPlayFabContext): void { 66 | var request: PlayFabServerModels.UpdatePlayerStatisticsRequest = { 67 | PlayFabId: currentPlayerId, Statistics: [{ 68 | StatisticName: "Level", 69 | Value: 2 70 | }] 71 | }; 72 | 73 | // The pre-defined "server" object has functions corresponding to each PlayFab server API 74 | // (https://api.playfab.com/Documentation/Server). It is automatically 75 | // authenticated as your title and handles all communication with 76 | // the PlayFab API, so you don't have to write extra code to issue HTTP requests. 77 | var playerStatResult = server.UpdatePlayerStatistics(request); 78 | } 79 | handlers["makeAPICall"] = MakeApiCall; 80 | 81 | // This is a simple example of making a web request to an external HTTP API. 82 | var MakeHttpRequest = function (args: any, context: IPlayFabContext): IMakeHttpRequestResponse { 83 | var headers: { [key: string]: string } = { 84 | "X-MyCustomHeader": "Some Value" 85 | }; 86 | 87 | var body = { 88 | input: args, 89 | userId: currentPlayerId, 90 | mode: "foobar" 91 | }; 92 | 93 | var url = "http://httpbin.org/status/200"; 94 | var content = JSON.stringify(body); 95 | var httpMethod = "post"; 96 | var contentType = "application/json"; 97 | 98 | // The pre-defined http object makes synchronous HTTP requests 99 | var response = http.request(url, httpMethod, content, contentType, headers); 100 | return { responseContent: response }; 101 | } 102 | interface IMakeHttpRequestResponse { 103 | responseContent: string; 104 | } 105 | handlers["makeHTTPRequest"] = MakeHttpRequest; 106 | 107 | // This is a simple example of a function that is called from a 108 | // PlayStream event action. (https://playfab.com/introducing-playstream/) 109 | var HandlePlayStreamEventAndProfile = function (args: any, context: IPlayFabContext): IHandlePlayStreamEventAndProfileResponse { 110 | 111 | // The event that triggered the action 112 | // (https://api.playfab.com/playstream/docs/PlayStreamEventModels) 113 | var psEvent = context.playStreamEvent; 114 | 115 | // The profile data of the player associated with the event 116 | // (https://api.playfab.com/playstream/docs/PlayStreamProfileModels) 117 | var profile = context.playerProfile; 118 | 119 | // Post data about the event to an external API 120 | var content = JSON.stringify({ user: profile.PlayerId, event: psEvent.EventName }); 121 | var response = http.request('https://httpbin.org/status/200', 'post', content, 'application/json', null); 122 | 123 | return { externalAPIResponse: response }; 124 | } 125 | interface IHandlePlayStreamEventAndProfileResponse { 126 | externalAPIResponse: string; 127 | } 128 | handlers["handlePlayStreamEventAndProfile"] = HandlePlayStreamEventAndProfile; 129 | 130 | // Below are some examples of using Cloud Script in slightly more realistic scenarios 131 | 132 | // This is a function that the game client would call whenever a player completes 133 | // a level. It updates a setting in the player's data that only game server 134 | // code can write - it is read-only on the client - and it updates a player 135 | // statistic that can be used for leaderboards. 136 | // 137 | // A funtion like this could be extended to perform validation on the 138 | // level completion data to detect cheating. It could also do things like 139 | // award the player items from the game catalog based on their performance. 140 | var CompletedLevel = function (args: any, context: IPlayFabContext): void { 141 | var level = args.levelName; 142 | var monstersKilled = args.monstersKilled; 143 | 144 | var updateUserDataResult = server.UpdateUserInternalData({ 145 | PlayFabId: currentPlayerId, 146 | Data: { 147 | lastLevelCompleted: level 148 | } 149 | }); 150 | 151 | log.debug("Set lastLevelCompleted for player " + currentPlayerId + " to " + level); 152 | 153 | var request: PlayFabServerModels.UpdatePlayerStatisticsRequest = { 154 | PlayFabId: currentPlayerId, Statistics: [{ 155 | StatisticName: "level_monster_kills", 156 | Value: monstersKilled 157 | }] 158 | }; 159 | server.UpdatePlayerStatistics(request); 160 | 161 | log.debug("Updated level_monster_kills stat for player " + currentPlayerId + " to " + monstersKilled); 162 | } 163 | handlers["completedLevel"] = CompletedLevel; 164 | 165 | // In addition to the Cloud Script handlers, you can define your own functions and call them from your handlers. 166 | // This makes it possible to share code between multiple handlers and to improve code organization. 167 | var UpdatePlayerMove = function (args): IUpdatePlayerMoveResponse { 168 | var validMove = processPlayerMove(args); 169 | return { validMove: validMove }; 170 | } 171 | interface IUpdatePlayerMoveResponse { 172 | validMove: boolean; 173 | } 174 | handlers["updatePlayerMove"] = UpdatePlayerMove; 175 | 176 | // This is a helper function that verifies that the player's move wasn't made 177 | // too quickly following their previous move, according to the rules of the game. 178 | // If the move is valid, then it updates the player's statistics and profile data. 179 | // This function is called from the "UpdatePlayerMove" handler above and also is 180 | // triggered by the "RoomEventRaised" Photon room event in the Webhook handler 181 | // below. 182 | // 183 | // For this example, the script defines the cooldown period (playerMoveCooldownInSeconds) 184 | // as 15 seconds. A recommended approach for values like this would be to create them in Title 185 | // Data, so that they can be queries in the script with a call to GetTitleData 186 | // (https://api.playfab.com/Documentation/Server/method/GetTitleData). This would allow you to 187 | // make adjustments to these values over time, without having to edit, test, and roll out an 188 | // updated script. 189 | function processPlayerMove(playerMove): boolean { 190 | var now = Date.now(); 191 | var playerMoveCooldownInSeconds = 15; 192 | 193 | var playerData = server.GetUserInternalData({ 194 | PlayFabId: currentPlayerId, 195 | Keys: ["last_move_timestamp"] 196 | }); 197 | 198 | var lastMoveTimestampSetting = playerData.Data["last_move_timestamp"]; 199 | 200 | if (lastMoveTimestampSetting) { 201 | var lastMoveTime = Date.parse(lastMoveTimestampSetting.Value); 202 | var timeSinceLastMoveInSeconds = (now - lastMoveTime) / 1000; 203 | log.debug("lastMoveTime: " + lastMoveTime + " now: " + now + " timeSinceLastMoveInSeconds: " + timeSinceLastMoveInSeconds); 204 | 205 | if (timeSinceLastMoveInSeconds < playerMoveCooldownInSeconds) { 206 | log.error("Invalid move - time since last move: " + timeSinceLastMoveInSeconds + "s less than minimum of " + playerMoveCooldownInSeconds + "s.") 207 | return false; 208 | } 209 | } 210 | 211 | var playerStats = server.GetPlayerStatistics({ 212 | PlayFabId: currentPlayerId 213 | }).Statistics; 214 | 215 | var movesMade = 0; 216 | for (var i = 0; i < playerStats.length; i++) 217 | if (playerStats[i].StatisticName === "") 218 | movesMade = playerStats[i].Value; 219 | movesMade += 1; 220 | 221 | var request: PlayFabServerModels.UpdatePlayerStatisticsRequest = { 222 | PlayFabId: currentPlayerId, Statistics: [{ 223 | StatisticName: "movesMade", 224 | Value: movesMade 225 | }] 226 | }; 227 | server.UpdatePlayerStatistics(request); 228 | 229 | server.UpdateUserInternalData({ 230 | PlayFabId: currentPlayerId, 231 | Data: { 232 | last_move_timestamp: new Date(now).toUTCString(), 233 | last_move: JSON.stringify(playerMove) 234 | } 235 | }); 236 | 237 | return true; 238 | } 239 | 240 | // This is an example of using PlayStream real-time segmentation to trigger 241 | // game logic based on player behavior. (https://playfab.com/introducing-playstream/) 242 | // The function is called when a player_statistic_changed PlayStream event causes a player 243 | // to enter a segment defined for high skill players. It sets a key value in 244 | // the player's internal data which unlocks some new content for the player. 245 | var UnlockHighSkillContent = function (args: any, context: IPlayFabContext): IUnlockHighSkillContentResponse { 246 | var playerStatUpdatedEvent = (context.playStreamEvent as PlayStreamModels.player_statistic_changed); 247 | 248 | var request: PlayFabServerModels.UpdateUserInternalDataRequest = { 249 | PlayFabId: currentPlayerId, 250 | Data: { 251 | "HighSkillContent": "true", 252 | "XPAtHighSkillUnlock": playerStatUpdatedEvent.StatisticValue.toString() 253 | } 254 | }; 255 | var playerInternalData = server.UpdateUserInternalData(request); 256 | 257 | log.info('Unlocked HighSkillContent for ' + context.playerProfile.DisplayName); 258 | return { profile: context.playerProfile }; 259 | } 260 | interface IUnlockHighSkillContentResponse { 261 | profile: IPlayFabPlayerProfile; 262 | } 263 | handlers["unlockHighSkillContent"] = UnlockHighSkillContent; 264 | 265 | // Photon Webhooks Integration 266 | // 267 | // The following functions are examples of Photon Cloud Webhook handlers. 268 | // When you enable the Photon Add-on (https://playfab.com/marketplace/photon/) 269 | // in the Game Manager, your Photon applications are automatically configured 270 | // to authenticate players using their PlayFab accounts and to fire events that 271 | // trigger your Cloud Script Webhook handlers, if defined. 272 | // This makes it easier than ever to incorporate multiplayer server logic into your game. 273 | 274 | 275 | // Triggered automatically when a Photon room is first created 276 | var RoomCreated = function (args): void { 277 | log.debug("Room Created - Game: " + args.GameId + " MaxPlayers: " + args.CreateOptions.MaxPlayers); 278 | } 279 | handlers["RoomCreated"] = RoomCreated; 280 | 281 | // Triggered automatically when a player joins a Photon room 282 | var RoomJoined = function (args): void { 283 | log.debug("Room Joined - Game: " + args.GameId + " PlayFabId: " + args.UserId); 284 | } 285 | handlers["RoomJoined"] = RoomJoined; 286 | 287 | // Triggered automatically when a player leaves a Photon room 288 | var RoomLeft = function (args): void { 289 | log.debug("Room Left - Game: " + args.GameId + " PlayFabId: " + args.UserId); 290 | } 291 | handlers["RoomLeft"] = RoomLeft; 292 | 293 | // Triggered automatically when a Photon room closes 294 | // Note: currentPlayerId is undefined in this function 295 | var RoomClosed = function (args): void { 296 | log.debug("Room Closed - Game: " + args.GameId); 297 | } 298 | handlers["RoomClosed"] = RoomClosed; 299 | 300 | // Triggered automatically when a Photon room game property is updated. 301 | // Note: currentPlayerId is undefined in this function 302 | var RoomPropertyUpdated = function (args): void { 303 | log.debug("Room Property Updated - Game: " + args.GameId); 304 | } 305 | handlers["RoomPropertyUpdated"] = RoomPropertyUpdated; 306 | 307 | // Triggered by calling "OpRaiseEvent" on the Photon client. The "args.Data" property is 308 | // set to the value of the "customEventContent" HashTable parameter, so you can use 309 | // it to pass in arbitrary data. 310 | var RoomEventRaised = function (args): void { 311 | var eventData = args.Data; 312 | log.debug("Event Raised - Game: " + args.GameId + " Event Type: " + eventData.eventType); 313 | 314 | switch (eventData.eventType) { 315 | case "playerMove": 316 | processPlayerMove(eventData); 317 | break; 318 | 319 | default: 320 | break; 321 | } 322 | } 323 | handlers["RoomEventRaised"] = RoomEventRaised; -------------------------------------------------------------------------------- /ExampleCloudScript.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////// 2 | // JenkinsConsoleUtility CloudScript functions 3 | /////////////////////////////////////////////// 4 | var TEST_TITLE_ID = "6195"; // NOTE: Replace this with your own titleID - DeleteUsers has an additional security check to avoid accidents 5 | var TEST_DATA_KEY = "TEST_DATA_KEY"; // Used to reuse args.customId, but it was kindof a pain, and made things fragile 6 | var HelloWorld = function (args, context) { 7 | var message = "Hello " + currentPlayerId + "!"; 8 | log.info(message); 9 | var inputValue = null; 10 | if (args && args.hasOwnProperty("inputValue")) 11 | inputValue = args.inputValue; 12 | log.debug("helloWorld:", { input: inputValue }); 13 | return { messageValue: message }; 14 | }; 15 | handlers["helloWorld"] = HelloWorld; 16 | var ThrowError = function (args) { 17 | var testObject = undefined; 18 | var failureObj = testObject.doesnotexist.doesnotexist; 19 | return failureObj; // Can't get to here 20 | }; 21 | handlers["throwError"] = ThrowError; 22 | var EasyLogEvent = function (args) { 23 | log.info(JSON.stringify(args.logMessage)); 24 | }; 25 | handlers["easyLogEvent"] = EasyLogEvent; 26 | /////////////////////////////////////////////// 27 | // JenkinsConsoleUtility CloudScript functions 28 | /////////////////////////////////////////////// 29 | var TestDataExists = function (args) { 30 | var playerData = server.GetUserInternalData({ 31 | PlayFabId: currentPlayerId, 32 | Keys: [TEST_DATA_KEY] 33 | }); 34 | return playerData.Data.hasOwnProperty(TEST_DATA_KEY); 35 | }; 36 | handlers["TestDataExists"] = TestDataExists; 37 | var GetTestData = function (args) { 38 | var testResults = null; 39 | var playerData = server.GetUserInternalData({ 40 | PlayFabId: currentPlayerId, 41 | Keys: [TEST_DATA_KEY] 42 | }); 43 | if (playerData.Data.hasOwnProperty(TEST_DATA_KEY)) { 44 | log.info("Returning Data: " + playerData.Data[TEST_DATA_KEY].Value); 45 | testResults = JSON.parse(playerData.Data[TEST_DATA_KEY].Value); 46 | var data = {}; 47 | data[TEST_DATA_KEY] = null; 48 | server.UpdateUserInternalData({ 49 | PlayFabId: currentPlayerId, 50 | Data: data 51 | }); 52 | } 53 | else { 54 | log.info("Expected data not found in: " + JSON.stringify(playerData)); 55 | } 56 | return testResults; 57 | }; 58 | handlers["GetTestData"] = GetTestData; 59 | var SaveTestData = function (args) { 60 | var data = {}; 61 | data[TEST_DATA_KEY] = JSON.stringify(args.testReport); 62 | log.info("Saving Data (" + currentPlayerId + "): " + JSON.stringify(data)); 63 | server.UpdateUserInternalData({ 64 | PlayFabId: currentPlayerId, 65 | Data: data 66 | }); 67 | }; 68 | handlers["SaveTestData"] = SaveTestData; 69 | //# sourceMappingURL=ExampleCloudScript.js.map -------------------------------------------------------------------------------- /ExampleCloudScript.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////// 2 | // JenkinsConsoleUtility CloudScript functions 3 | /////////////////////////////////////////////// 4 | 5 | var TEST_TITLE_ID: string = "6195"; // NOTE: Replace this with your own titleID - DeleteUsers has an additional security check to avoid accidents 6 | var TEST_DATA_KEY: string = "TEST_DATA_KEY"; // Used to reuse args.customId, but it was kindof a pain, and made things fragile 7 | 8 | var HelloWorld = function (args: IHelloWorldRequest, context): IHelloWorldResult { 9 | var message: string = "Hello " + currentPlayerId + "!"; 10 | log.info(message); 11 | var inputValue: any = null; 12 | if (args && args.hasOwnProperty("inputValue")) 13 | inputValue = args.inputValue; 14 | log.debug("helloWorld:", { input: inputValue }); 15 | return { messageValue: message }; 16 | }; 17 | interface IHelloWorldRequest { 18 | inputValue?: any 19 | } 20 | interface IHelloWorldResult { 21 | messageValue: string 22 | } 23 | handlers["helloWorld"] = HelloWorld; 24 | 25 | var ThrowError = function (args: void): void { 26 | var testObject: any = undefined; 27 | var failureObj: any = testObject.doesnotexist.doesnotexist; 28 | return failureObj; // Can't get to here 29 | } 30 | handlers["throwError"] = ThrowError; 31 | 32 | var EasyLogEvent = function (args: IEasyLogEvent): void { 33 | log.info(JSON.stringify(args.logMessage)); 34 | }; 35 | interface IEasyLogEvent { 36 | logMessage: string 37 | } 38 | handlers["easyLogEvent"] = EasyLogEvent; 39 | 40 | /////////////////////////////////////////////// 41 | // JenkinsConsoleUtility CloudScript functions 42 | /////////////////////////////////////////////// 43 | 44 | var TestDataExists = function (args: void): boolean { 45 | var playerData = server.GetUserInternalData({ 46 | PlayFabId: currentPlayerId, 47 | Keys: [TEST_DATA_KEY] 48 | }); 49 | return playerData.Data.hasOwnProperty(TEST_DATA_KEY); 50 | }; 51 | handlers["TestDataExists"] = TestDataExists; 52 | 53 | var GetTestData = function (args: void): ITestReport { 54 | var testResults: ITestReport = null; 55 | var playerData = server.GetUserInternalData({ 56 | PlayFabId: currentPlayerId, 57 | Keys: [TEST_DATA_KEY] 58 | }); 59 | if (playerData.Data.hasOwnProperty(TEST_DATA_KEY)) { 60 | log.info("Returning Data: " + playerData.Data[TEST_DATA_KEY].Value); 61 | testResults = JSON.parse(playerData.Data[TEST_DATA_KEY].Value); 62 | var data: { [key: string]: string } = {}; 63 | data[TEST_DATA_KEY] = null; 64 | server.UpdateUserInternalData({ 65 | PlayFabId: currentPlayerId, 66 | Data: data 67 | }); 68 | } else { 69 | log.info("Expected data not found in: " + JSON.stringify(playerData)); 70 | } 71 | 72 | return testResults; 73 | }; 74 | handlers["GetTestData"] = GetTestData; 75 | 76 | var SaveTestData = function (args: ITestReportPackage): void { 77 | var data: { [key: string]: string } = {}; 78 | data[TEST_DATA_KEY] = JSON.stringify(args.testReport); 79 | log.info("Saving Data (" + currentPlayerId + "): " + JSON.stringify(data)); 80 | server.UpdateUserInternalData({ 81 | PlayFabId: currentPlayerId, 82 | Data: data 83 | }); 84 | } 85 | interface ITestReportPackage { 86 | testReport: ITestReport 87 | } 88 | interface ITestReport { 89 | // Actually very big/complicated, but not inspected at all in this code. 90 | } 91 | handlers["SaveTestData"] = SaveTestData; 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LiveOpsExample.ts: -------------------------------------------------------------------------------- 1 | interface StoreCycleTitleData { [key: string]: string[] } 2 | // It's important for this example to have a clear idea of what this data looks like 3 | // Your real data would only be in TitleData, stored as a json string in the "activeEvents" key 4 | var EXAMPLE_STORE_CYCLE: StoreCycleTitleData = { 5 | "daily": ["daily_monday", "daily_tuesday", "daily_wednesday", "daily_thursday", "daily_friday", "daily_saturday", "daily_sunday"], 6 | "weekly": ["weekly_red", "weekly_green", "weekly_blue"], 7 | "holiday": [null, "Thanksgiving"] 8 | } 9 | var DEBUG_ENABLED = true; // Allows you to call manually with ExecuteCloudScript. Set this to false in production 10 | 11 | // Read TitleData, getting live active events, and the static information about event cycles 12 | function GetTitleEventInfo() { 13 | var titleRequest: PlayFabServerModels.GetTitleDataRequest = { Keys: ["activeEvents", "storeCycles"] }; 14 | var titleResponse = server.GetTitleData(titleRequest); 15 | 16 | 17 | var activeEvents: string[] = null; 18 | if (titleResponse.Data.hasOwnProperty("activeEvents")) 19 | activeEvents = JSON.parse(titleResponse.Data["activeEvents"]); 20 | if (!activeEvents) 21 | activeEvents = []; 22 | 23 | var storeCycles: StoreCycleTitleData = null; 24 | if (titleResponse.Data.hasOwnProperty("storeCycles")) 25 | storeCycles = JSON.parse(titleResponse.Data["storeCycles"]);; 26 | if (!storeCycles) 27 | storeCycles = EXAMPLE_STORE_CYCLE; 28 | 29 | return { 30 | activeEvents: activeEvents, 31 | storeCycles: storeCycles 32 | }; 33 | } 34 | 35 | // Update TitleData, setting new live active events 36 | function SetTitleEventInfo(activeEvents: string[]) { 37 | var titleRequest: PlayFabServerModels.SetTitleDataRequest = { Key: "activeEvents", Value: JSON.stringify(activeEvents) }; 38 | server.SetTitleData(titleRequest); 39 | } 40 | 41 | function CycleEvent(cycleType: string, cycleTo: string = null): string[] { 42 | var eventInfo = GetTitleEventInfo(); 43 | var cycleList = eventInfo.storeCycles[cycleType]; 44 | 45 | var prevIndex: number = 0; 46 | for (var i = 0; i < cycleList.length; i++) { 47 | for (var j = 0; j < eventInfo.activeEvents.length; j++) { 48 | if (eventInfo.activeEvents[j] === cycleList[i]) { 49 | eventInfo.activeEvents.splice(j, 1); 50 | prevIndex = i; 51 | } 52 | } 53 | } 54 | 55 | if (!cycleTo) // Determine the next event if unspecified 56 | cycleTo = cycleList[(prevIndex + 1) % cycleList.length]; 57 | if (cycleTo) // Set the next event if defined 58 | eventInfo.activeEvents.push(cycleTo); 59 | 60 | SetTitleEventInfo(eventInfo.activeEvents); 61 | return eventInfo.activeEvents; 62 | } 63 | 64 | var CycleDailyEvent = function (args: any, context: any) { 65 | if (!DEBUG_ENABLED && !context) throw "This can only be called from PlayStream"; // Safety check to prevent Clients from changing events, and/or accidents 66 | return CycleEvent("daily"); 67 | } 68 | handlers["CycleDailyEvent"] = CycleDailyEvent; 69 | 70 | var CycleWeeklyEvent = function (args: any, context: any) { 71 | if (!DEBUG_ENABLED && !context) throw "This can only be called from PlayStream"; // Safety check to prevent Clients from changing events, and/or accidents 72 | return CycleEvent("weekly"); 73 | } 74 | handlers["CycleWeeklyEvent"] = CycleWeeklyEvent; 75 | 76 | var DisableHoliday = function (args: any, context: any) { 77 | if (!DEBUG_ENABLED && !context) throw "This can only be called from PlayStream"; // Safety check to prevent Clients from changing events, and/or accidents 78 | return CycleEvent("holiday", null); 79 | } 80 | handlers["DisableHoliday"] = DisableHoliday; 81 | 82 | // Each Holiday-Enable needs its own handler since context cannot contain any parameters. 83 | // You could use additional title-data to determine when to activate/deactivate holidays 84 | var EnableThanksgiving = function (args: any, context: any) { 85 | if (!DEBUG_ENABLED && !context) throw "This can only be called from PlayStream"; // Safety check to prevent Clients from changing events, and/or accidents 86 | return CycleEvent("holiday", "Thanksgiving"); 87 | } 88 | handlers["EnableThanksgiving"] = EnableThanksgiving; 89 | -------------------------------------------------------------------------------- /OtherExamples.ts: -------------------------------------------------------------------------------- 1 | const SELL_PRICE_RATIO: number = 0.75; 2 | 3 | function SellItem_internal(soldItemInstanceId: string, requestedVcType: string) { 4 | var inventory = server.GetUserInventory({ PlayFabId: currentPlayerId }); 5 | var itemInstance: PlayFabServerModels.ItemInstance = null; 6 | for (var i = 0; i < inventory.Inventory.length; i++) { 7 | if (inventory.Inventory[i].ItemInstanceId === soldItemInstanceId) 8 | itemInstance = inventory.Inventory[i]; 9 | } 10 | if (!itemInstance) throw "Item instance not found"; // Protection against client providing incorrect data 11 | 12 | var catalog = server.GetCatalogItems({ CatalogVersion: itemInstance.CatalogVersion }); 13 | var catalogItem: PlayFabServerModels.CatalogItem = null; 14 | for (var c = 0; c < catalog.Catalog.length; c++) { 15 | if (itemInstance.ItemId === catalog.Catalog[c].ItemId) 16 | catalogItem = catalog.Catalog[c]; 17 | } 18 | if (!catalogItem) throw "Catalog Item not found"; // Title catalog consistency check (You should never remove a catalog/catalogItem if any player owns that item 19 | 20 | var buyPrice: number = 0; 21 | if (catalogItem.VirtualCurrencyPrices.hasOwnProperty(requestedVcType)) 22 | buyPrice = catalogItem.VirtualCurrencyPrices[requestedVcType]; 23 | if (buyPrice <= 0) 24 | throw "Cannot redeem this item for: " + requestedVcType; // The client requested a virtual currency which doesn't apply to this item 25 | 26 | // Once we get here all safety checks are passed - Perform the sell 27 | var sellPrice: number = Math.floor(buyPrice * SELL_PRICE_RATIO); 28 | server.AddUserVirtualCurrency({ PlayFabId: currentPlayerId, Amount: sellPrice, VirtualCurrency: requestedVcType }); 29 | server.RevokeInventoryItem({ PlayFabId: currentPlayerId, ItemInstanceId: soldItemInstanceId }); 30 | } 31 | 32 | interface ISellItemArgs { 33 | soldItemInstanceId: string; 34 | requestedVcType: string; 35 | }; 36 | var SellItem = function (args: ISellItemArgs) { 37 | if (!args || !args.soldItemInstanceId || !args.requestedVcType) 38 | throw "Invalid input parameters, expected soldItemInstanceId and requestedVcType"; 39 | SellItem_internal(args.soldItemInstanceId, args.requestedVcType); 40 | } 41 | handlers["SellItem"] = SellItem; 42 | 43 | // Publisher data Examples 44 | const PUBLISHER_USED_TITLES_KEY = "playedTitleIds"; 45 | var TrackTitleUsage = function () { 46 | // Get the User Publisher Data for this player, and convert it into our expected format 47 | var getRequest: PlayFabServerModels.GetUserDataRequest = { Keys: [PUBLISHER_USED_TITLES_KEY], PlayFabId: currentPlayerId }; 48 | var getResult: PlayFabServerModels.GetUserDataResult = server.GetUserPublisherInternalData(getRequest); 49 | var playedTitlesList: string[] = JSON.parse(getResult.Data[PUBLISHER_USED_TITLES_KEY].Value); // format is arbitrary, but this example assumes string[] 50 | if (!playedTitlesList) 51 | playedTitlesList = []; 52 | 53 | // Check if we've played this title already 54 | var alreadyPlayed: boolean = false; 55 | for (var i = 0; i < playedTitlesList.length; i++) 56 | alreadyPlayed = alreadyPlayed || playedTitlesList[i] === script.titleId; 57 | if (alreadyPlayed) 58 | return; // Nothing to do 59 | 60 | // Update the list of played titles, and re-save 61 | playedTitlesList.push(script.titleId); 62 | var setRequest: PlayFabServerModels.UpdateUserDataRequest = { PlayFabId: currentPlayerId, Data: { PUBLISHER_USED_TITLES_KEY: JSON.stringify(playedTitlesList) } }; 63 | server.UpdateUserPublisherInternalData(setRequest); 64 | } 65 | handlers["TrackTitleUsage"] = TrackTitleUsage; 66 | 67 | const PUBLISHER_REDEEMED_TITLES_KEY = "redeemedTitleIds"; 68 | const MY_CROSS_TITLE_REWARDS: { [key: string]: number } = { "AU": 10 }; 69 | var CheckCrossTitleRewards = function () { 70 | // Get the publisher data concerning cross-title rewards for this player 71 | var getRequest: PlayFabServerModels.GetUserDataRequest = { Keys: [PUBLISHER_USED_TITLES_KEY, PUBLISHER_REDEEMED_TITLES_KEY], PlayFabId: currentPlayerId }; 72 | var getResult: PlayFabServerModels.GetUserDataResult = server.GetUserPublisherInternalData(getRequest); 73 | var redeemedTitleRewards: { [key: string]: string[] } = JSON.parse(getResult.Data[PUBLISHER_REDEEMED_TITLES_KEY].Value); // format is arbitrary, but this example assumes { [key: string]: string[] } 74 | if (!redeemedTitleRewards) 75 | redeemedTitleRewards = {}; 76 | var playedTitlesList: string[] = JSON.parse(getResult.Data[PUBLISHER_USED_TITLES_KEY].Value); // format is arbitrary, but this example assumes string[] 77 | if (!playedTitlesList) 78 | playedTitlesList = []; 79 | 80 | // Determine which titles are un-redeemed 81 | var unredeemedTitleIds: string[] = []; 82 | for (var i = 0; i < playedTitlesList.length; i++) { 83 | if (!redeemedTitleRewards.hasOwnProperty(playedTitlesList[i]) && playedTitlesList[i] !== script.titleId) 84 | unredeemedTitleIds.push(playedTitlesList[i]); 85 | } 86 | if (unredeemedTitleIds.length === 0) 87 | return null; // Nothing to redeem 88 | 89 | // Give the cross title rewards 90 | var multiplier: number = unredeemedTitleIds.length; 91 | var actualRewards: { [key: string]: number } = {}; // MY_CROSS_TITLE_REWARDS is a global constant, so don't modify it or you'll mess up future calls 92 | for (var eachKey in MY_CROSS_TITLE_REWARDS) { 93 | actualRewards[eachKey] = MY_CROSS_TITLE_REWARDS[eachKey] * multiplier; 94 | server.AddUserVirtualCurrency({ PlayFabId: currentPlayerId, VirtualCurrency: eachKey, Amount: MY_CROSS_TITLE_REWARDS[eachKey] }); // Can only add 1 VC at a time 95 | } 96 | // Save the Publisher data indicating rewards are claimed 97 | redeemedTitleRewards[script.titleId] = playedTitlesList; 98 | var setRequest: PlayFabServerModels.UpdateUserDataRequest = { PlayFabId: currentPlayerId, Data: { PUBLISHER_REDEEMED_TITLES_KEY: JSON.stringify(redeemedTitleRewards) } }; 99 | server.UpdateUserPublisherInternalData(setRequest); 100 | 101 | // Tell the client the reward 102 | return actualRewards; 103 | } 104 | handlers["CheckCrossTitleRewards"] = CheckCrossTitleRewards; 105 | 106 | const MY_GAME_GROUP_KEYS: string[] = ["gameState", "currentPlayerTurn"]; 107 | interface PlayerTurnArgs { 108 | sharedGroupId: string; 109 | nextPlayerTurn: string; 110 | turnData: any; 111 | } 112 | var TakePlayerTurn = function (args: PlayerTurnArgs) { 113 | var getRequest: PlayFabServerModels.GetSharedGroupDataRequest = { SharedGroupId: args.sharedGroupId, GetMembers: true, Keys: MY_GAME_GROUP_KEYS }; 114 | var gameData: PlayFabServerModels.GetSharedGroupDataResult = server.GetSharedGroupData(getRequest); 115 | CheckValidPlayer(currentPlayerId, args.sharedGroupId, gameData.Members, gameData.Data["currentPlayerTurn"].Value, args.nextPlayerTurn); 116 | var newGameStateJson = UpdateGameState(args.turnData, gameData.Data["gameState"].Value); 117 | var updateRequest: PlayFabServerModels.UpdateSharedGroupDataRequest = { 118 | SharedGroupId: args.sharedGroupId, 119 | Data: { 120 | "gameState": newGameStateJson, 121 | "currentPlayerTurn": args.nextPlayerTurn 122 | } 123 | }; 124 | server.UpdateSharedGroupData(updateRequest); 125 | } 126 | handlers["TakePlayerTurn"] = TakePlayerTurn; 127 | 128 | function CheckValidPlayer(playFabId: string, sharedGroupId: string, members: string[], currentPlayerTurn: string, nextPlayerTurn: string): void { 129 | var validCurPlayer = false; 130 | var validNextPlayer = false; 131 | for (var m = 0; m < members.length; m++) { 132 | if (members[m] === playFabId) 133 | validCurPlayer = true; 134 | if (members[m] === nextPlayerTurn) 135 | validNextPlayer = true; 136 | } 137 | if (!validCurPlayer || !validNextPlayer) // Take extreme action against a player trying to cheat 138 | { 139 | server.BanUsers({ Bans: [{ PlayFabId: playFabId, Reason: "Trying to play a game you don't belong to: " + sharedGroupId }] }); 140 | throw "You have been banned"; 141 | } 142 | 143 | if (playFabId !== currentPlayerTurn) 144 | // May wish to additionally implement a spam-counter here and potentially take more extreme action for high-spam count 145 | throw "Not your turn"; 146 | } 147 | function UpdateGameState(turnData: any, currentState: string): string { 148 | // PSEUDO-CODE-STUB: Update the turn-based game state according to the rules of this game 149 | return JSON.stringify({}); 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sdk-Testing Cloud Script README 2 | 3 | Cloud Script example, and TypeScript typings for PlayFab Cloud Script Environment 4 | 5 | Cloud Script is optional. It allows you to define logic that runs on a secure PlayFab server. TypeScript is additionally optional, as it allows you to write your Cloud Script with additional validation. 6 | 7 | 8 | ## 1. Community Support 9 | 10 | This is a community supported set of samples. 11 | 12 | For new and existing users, you can use the current version as it is. The team at Microsoft would no longer be providing official support for those using this SDK. You can continue to get community support and updates at [PlayFab forums](https://community.playfab.com/index.html). 13 | 14 | We are currently looking for reliable community partners to provide long-term support for this SDK. If you are interested to take ownership and provide future maintenance, let us know. 15 | 16 | What you have to do: 17 | * Fork this repo 18 | * Push your updates 19 | * Make sure you follow the Apache License, Version 2.0 guidelines for reproduction and modification, and document that Microsoft PlayFab is the original creator 20 | * Go to [PlayFab forums](https://community.playfab.com/index.html) 21 | * Write a post with a link to your forked repo so everyone knows about them 22 | 23 | We're excited to hear from you. Thank you for your support and happy coding. 24 | 25 | ## 2. Overview: 26 | 27 | This project serves several purposes: 28 | 29 | 1. Demonstrate a repository linked to your PlayFab title, which automatically populates the Cloud Script for your title 30 | * All *.js files are merged, and then that merged file becomes your title's Cloud Script 31 | 2. Provide you a working [Cloud Script file](ExampleCloudScript.js) 32 | 3. Demonstrate the [Cloud Script typings file](https://github.com/PlayFab/SdkTestingCloudScript/blob/master/Scripts/typings/PlayFab/CloudScript.d.ts) which will define the PlayFab environment for you 33 | 34 | 35 | ## 3. Using Cloud Script 36 | 37 | For more information about Cloud Script, consult [our guide](https://api.playfab.com/docs/using-cloud-script/) 38 | 39 | 40 | ## 4. TypeScript 41 | 42 | The PlayFab Cloud Script feature does not utilize TypeScript directly. TypeScript is a superset of JavaScript, which provides strong typing and validation for JavaScript. You can write your Cloud Script in TypeScript, generate the corresponding JavaScript, and submit the JavaScript to your title. 43 | 44 | [Here is a starting place if you wish to learn TypeScript](http://www.typescriptlang.org/) 45 | 46 | 47 | ## 5. Acknowledgements 48 | 49 | [CloudScript.d.ts](https://github.com/PlayFab/SdkTestingCloudScript/blob/master/Scripts/typings/PlayFab/CloudScript.d.ts) was created using our SdkGenerator by [Joshua Strunk](https://joshuastrunk.com/) @ [Flying Car Games Inc](http://flyingcargames.com/) 50 | 51 | 52 | ## 6. Troubleshooting: 53 | 54 | Our Developer Success Team can assist with answering any questions as well as process any feedback you have about PlayFab services. 55 | 56 | [Forums, Support and Knowledge Base](https://community.playfab.com/index.html) 57 | 58 | 59 | ## 7. Copyright and Licensing Information: 60 | 61 | Apache License -- 62 | Version 2.0, January 2004 63 | http://www.apache.org/licenses/ 64 | 65 | Full details available within the LICENSE file. 66 | -------------------------------------------------------------------------------- /Scripts/Entity.ts: -------------------------------------------------------------------------------- 1 | function GetEntityToken(params: any, context: IPlayFabContext): void { 2 | var getTokenRequest: PlayFabAuthenticationModels.GetEntityTokenRequest = {}; 3 | var getTokenResponse: PlayFabAuthenticationModels.GetEntityTokenResponse = entity.GetEntityToken(getTokenRequest); 4 | var entityId: string = getTokenResponse.Entity.Id; 5 | var entityType: string = getTokenResponse.Entity.Type; 6 | } 7 | handlers.GetEntityToken = GetEntityToken; 8 | 9 | function GetObjects(params: any, context: IPlayFabContext) { 10 | var getObjRequest: PlayFabDataModels.GetObjectsRequest = { 11 | Entity: { 12 | Id: params.entityId, 13 | Type: params.entityType 14 | } 15 | }; 16 | var getObjResponse: PlayFabDataModels.GetObjectsResponse = entity.GetObjects(getObjRequest); 17 | var entityId: string = getObjResponse.Entity.Id; 18 | var entityType: string = getObjResponse.Entity.Type; 19 | var entityObjs: PlayFabDataModels.ObjectResult = getObjResponse.Objects["testKey"]; 20 | } 21 | handlers.GetObjects = GetObjects; 22 | 23 | -------------------------------------------------------------------------------- /TitleAB.ts: -------------------------------------------------------------------------------- 1 | // Special key in the Title Data that contains an array of AB buckets that participate in the testing 2 | var TITLE_AB_TEST_TITLE_KEY = "TitleDataAbTestSegmentIds"; 3 | 4 | var GetTitleDataAB = function (args, ctx): string { 5 | // The data key the player originally requested. 6 | var dataKey: string = args.TitleKey; 7 | 8 | // A variable to store AB segment of the player, if any 9 | var currentAbTestSegmentId: string = null; 10 | 11 | /* 12 | * We store a list of bucket IDs that participate in the AB testing in the title data. 13 | * This line extracts an array of such ids 14 | */ 15 | var requestedTitleData = server.GetTitleData({ Keys: [TITLE_AB_TEST_TITLE_KEY, dataKey] }); 16 | var defaultValue: string = requestedTitleData.Data.hasOwnProperty(dataKey) ? requestedTitleData.Data[dataKey] : null; 17 | var segmentIdJson: string = requestedTitleData.Data.hasOwnProperty(TITLE_AB_TEST_TITLE_KEY) ? requestedTitleData.Data[TITLE_AB_TEST_TITLE_KEY] : null; 18 | var abTestSegmentIds: string[] = JSON.parse(segmentIdJson); 19 | 20 | // This line extracts all the segments current player belongs to 21 | var playerSegments = server.GetPlayerSegments({ PlayFabId: currentPlayerId }).Segments; 22 | 23 | // Locate first ABTest segment the player belongs to 24 | for (var i = 0; i < playerSegments.length; i++) { 25 | var playerSegmentId: string = playerSegments[i].Id; 26 | if (abTestSegmentIds.indexOf(playerSegmentId) !== -1) 27 | currentAbTestSegmentId = playerSegmentId; 28 | } 29 | 30 | // If player does not belong to any tested segment, return a value for the original key 31 | if (!currentAbTestSegmentId) 32 | return defaultValue; 33 | 34 | /* 35 | * If player belongs to one of AB tested segments 36 | * we use ID of this segment to construct special key 37 | * First part of this key is the original key 38 | * Followed by underscore ('-') we add a suffix, which is ID of the bucket the player belongs to. 39 | */ 40 | var abTestedKey: string = dataKey + "_" + currentAbTestSegmentId; 41 | 42 | // We try to get a value using our special key 43 | var result = server.GetTitleData({ Keys: [abTestedKey] }); 44 | 45 | if (result.Data[abTestedKey]) // if we have data defined for this bucket, we return it 46 | return result.Data[abTestedKey]; 47 | else // Otherwise, we return the value for the original key 48 | return defaultValue; 49 | } 50 | handlers["GetTitleDataAB"] = GetTitleDataAB; -------------------------------------------------------------------------------- /UnicornBattle.js: -------------------------------------------------------------------------------- 1 | var defaultCatalog = "CharacterClasses"; 2 | var GEM_CURRENCY_CODE = "GM"; 3 | var GOLD_CURRENCY_CODE = "AU"; 4 | var HEART_CURRENCY_CODE = "HT"; 5 | ///////////////////////// Cloud Script Handler Functions ///////////////////////// 6 | function CreateCharacter(args) { 7 | var grantItemsRequest = { 8 | PlayFabId: currentPlayerId, 9 | CatalogVersion: defaultCatalog, 10 | ItemIds: [args.catalogCode] 11 | }; 12 | server.GrantItemsToUser(grantItemsRequest); 13 | var grantCharRequest = { 14 | PlayFabId: currentPlayerId, 15 | CharacterName: args.characterName, 16 | CharacterType: args.catalogCode 17 | }; 18 | var result = server.GrantCharacterToUser(grantCharRequest); 19 | InitializeNewCharacterData(result.CharacterId, args.catalogCode); // set up default character data 20 | return true; 21 | } 22 | function DeleteCharacter(args) { 23 | var deleteRequest = { 24 | PlayFabId: currentPlayerId, 25 | CharacterId: args.characterId, 26 | SaveCharacterInventory: false 27 | }; 28 | server.DeleteCharacterFromUser(deleteRequest); 29 | return true; 30 | } 31 | function GetBaseClassForType(args) { 32 | var getTitleDataRequest = { Keys: ["Classes"] }; 33 | var result = server.GetTitleData(getTitleDataRequest); 34 | var classes = JSON.parse(result.Data["Classes"]); 35 | for (var each in classes) 36 | if (classes[each].CatalogCode === args.cCode) 37 | return classes[each]; 38 | return null; 39 | } 40 | function SaveProgress(args) { 41 | args.CurrentPlayerData = !args.CurrentPlayerData ? {} : args.CurrentPlayerData; 42 | args.QuestProgress = !args.QuestProgress ? {} : args.QuestProgress; 43 | args.LevelRamp = !args.LevelRamp ? {} : args.LevelRamp; 44 | //check for level up 45 | var baseStats = args.CurrentPlayerData.baseClass; 46 | var characterData = args.CurrentPlayerData.characterData; 47 | var vitals = args.CurrentPlayerData.PlayerVitals; 48 | var questProgress = args.QuestProgress; 49 | var experienceLevel = "" + characterData.CharacterLevel; 50 | var experienceTarget = args.LevelRamp[experienceLevel]; // int 51 | if (vitals.didLevelUp) { 52 | // increment the spell 53 | if (vitals.skillSelected === 0) 54 | characterData.Spell1_Level++; 55 | else if (vitals.skillSelected === 1) 56 | characterData.Spell2_Level++; 57 | else 58 | characterData.Spell3_Level++; 59 | //Update stats 60 | characterData.CharacterLevel++; 61 | characterData.Defense += baseStats.DPLevelBonus; 62 | characterData.Speed += baseStats.SPLevelBonus; 63 | characterData.Mana += baseStats.MPLevelBonus; 64 | characterData.Health += baseStats.HPLevelBonus; 65 | characterData.TotalExp += questProgress.XpCollected; 66 | characterData.ExpThisLevel = characterData.TotalExp - experienceTarget; 67 | } 68 | else { 69 | characterData.ExpThisLevel += questProgress.XpCollected; 70 | } 71 | //check for achievements & offers 72 | EvaluateAchievements(args.CurrentPlayerData.characterDetails.CharacterId); 73 | // API params 74 | var updateDataRequest = { 75 | PlayFabId: currentPlayerId, 76 | CharacterId: args.CurrentPlayerData.characterDetails.CharacterId, 77 | Data: { CharacterData: JSON.stringify(characterData) }, 78 | Permission: "Public" 79 | }; 80 | server.UpdateCharacterReadOnlyData(updateDataRequest); 81 | // set up Gold VC 82 | var addVcRequest = { 83 | PlayFabId: currentPlayerId, 84 | VirtualCurrency: GOLD_CURRENCY_CODE, 85 | Amount: questProgress.GoldCollected 86 | }; 87 | server.AddUserVirtualCurrency(addVcRequest); 88 | } 89 | function RetriveQuestItems(args) { 90 | var grantRequest = { 91 | PlayFabId: currentPlayerId, 92 | ItemIds: args.ItemIds 93 | }; 94 | var response = server.GrantItemsToUser(grantRequest); 95 | return JSON.stringify(response.ItemGrantResults); 96 | } 97 | function SubtractLife() { 98 | var subtractVcRequest = { 99 | PlayFabId: currentPlayerId, 100 | VirtualCurrency: HEART_CURRENCY_CODE, 101 | Amount: 1 102 | }; 103 | return server.SubtractUserVirtualCurrency(subtractVcRequest); 104 | } 105 | function EnableValentinesEvent() { 106 | SetEventActive("evalentine", true); 107 | } 108 | function DisableValentinesEvent() { 109 | SetEventActive("evalentine", false); 110 | } 111 | function EnablePresEvent() { 112 | SetEventActive("epresident", true); 113 | } 114 | function DisablePresEvent() { 115 | SetEventActive("epresident", false); 116 | } 117 | ///////////////////////// HELPER FUNCTIONS (NOT DIRECTLY CALLABLE FROM THE CLIENT) ///////////////////////// 118 | function InitializeNewCharacterData(characterId, catalogItemId) { 119 | var cDetails = GetBaseClassForType({ cCode: catalogItemId }); 120 | // default character properties 121 | var CharacterData = { 122 | ClassDetails: cDetails, 123 | TotalExp: 0, 124 | ExpThisLevel: 0, 125 | Health: cDetails.BaseHP, 126 | Mana: cDetails.BaseMP, 127 | Defense: cDetails.BaseDP, 128 | Speed: cDetails.BaseSP, 129 | CharacterLevel: 1, 130 | Spell1_Level: 0, 131 | Spell2_Level: 0, 132 | Spell3_Level: 0, 133 | CustomAvatar: null 134 | }; 135 | // Char Data 136 | var updateDataRequest = { 137 | PlayFabId: currentPlayerId, 138 | CharacterId: characterId, 139 | Data: { CharacterData: JSON.stringify(CharacterData) }, 140 | Permission: "Public" 141 | }; 142 | server.UpdateCharacterReadOnlyData(updateDataRequest); 143 | // set up Heart VC 144 | var vcHeartRequest = { 145 | PlayFabId: currentPlayerId, 146 | VirtualCurrency: HEART_CURRENCY_CODE, 147 | Amount: 0 148 | }; 149 | server.AddUserVirtualCurrency(vcHeartRequest); 150 | // set up Gold VC 151 | var vcGoldRequest = { 152 | PlayFabId: currentPlayerId, 153 | VirtualCurrency: GOLD_CURRENCY_CODE, 154 | Amount: 0 155 | }; 156 | server.AddUserVirtualCurrency(vcGoldRequest); 157 | // set up Gem VC 158 | var vcGemRequest = { 159 | PlayFabId: currentPlayerId, 160 | VirtualCurrency: GEM_CURRENCY_CODE, 161 | Amount: 0 162 | }; 163 | server.AddUserVirtualCurrency(vcGemRequest); 164 | } 165 | function HasAchievement(achievement, playerStats) { 166 | if (achievement.SingleStat) { 167 | for (var stat in playerStats) 168 | if (playerStats.hasOwnProperty(stat) && stat.indexOf(achievement.MatchingStatistic) > -1 169 | && playerStats[stat] >= achievement.Threshold) 170 | // Stat found and exceeds the achievement threshold 171 | return true; 172 | } 173 | else { 174 | // process aggregate stats 175 | var statTotal = 0; 176 | for (var stat in playerStats) 177 | if (playerStats.hasOwnProperty(stat) && stat.indexOf(achievement.MatchingStatistic) > -1) 178 | statTotal += playerStats[stat]; 179 | if (statTotal >= achievement.Threshold) 180 | return true; // sum of stats found exceeds the achievement threshold 181 | } 182 | return false; 183 | } 184 | function EvaluateAchievements(characterId) { 185 | // get the achievement thresholds set by TitleData 186 | var getTitleAchievementsRequest = { Keys: ["Achievements"] }; 187 | var titleDataResult = server.GetTitleData(getTitleAchievementsRequest); 188 | var serverAchievements; 189 | if (titleDataResult.Data.hasOwnProperty("Achievements")) 190 | serverAchievements = JSON.parse(titleDataResult.Data["Achievements"]); 191 | else 192 | throw "Achievements not found on Server. Check TitleData[\"Achievements\"]"; 193 | // get the character stats 194 | var statsRequest = { 195 | PlayFabId: currentPlayerId, 196 | CharacterId: characterId, 197 | }; 198 | var statsResult = server.GetCharacterStatistics(statsRequest); 199 | var charStats = statsResult.CharacterStatistics; 200 | // get the unlocked stats for the character 201 | var getCharDataRequest = { 202 | PlayFabId: currentPlayerId, 203 | CharacterId: characterId, 204 | Keys: ["Achievements"] 205 | }; 206 | var charDataResult = server.GetCharacterReadOnlyData(getCharDataRequest); 207 | var awardedAchievements; 208 | if (charDataResult.Data.hasOwnProperty("Achievements")) 209 | awardedAchievements = JSON.parse(charDataResult.Data["Achievements"].Value); 210 | else 211 | awardedAchievements = []; // no achievements 212 | // TODO -- need to track new achievements and toss them over to the offer evaluator 213 | var arrayCount = awardedAchievements.length; 214 | for (var achievementName in serverAchievements) 215 | // if the player does not already have the achievement, evaluate progress 216 | if (awardedAchievements.indexOf(achievementName) === -1 && HasAchievement(serverAchievements[achievementName], charStats)) 217 | awardedAchievements.push(achievementName); 218 | // if we added some new achievements, save details to character data 219 | if (arrayCount < awardedAchievements.length) { 220 | var updateCharDataRequest = { 221 | PlayFabId: currentPlayerId, 222 | CharacterId: characterId, 223 | Data: { Achievements: JSON.stringify(awardedAchievements) }, 224 | Permission: "Public" 225 | }; 226 | server.UpdateCharacterReadOnlyData(updateCharDataRequest); 227 | } 228 | } 229 | var EVENT_TITLE_DATA_KEY = "ActiveEventKeys"; 230 | function SetEventActive(eventKey, isActive) { 231 | var getRequest = { Keys: [EVENT_TITLE_DATA_KEY] }; 232 | var serverData = server.GetTitleData(getRequest); 233 | var eventKeys = JSON.parse(serverData.Data[EVENT_TITLE_DATA_KEY]); 234 | if (isActive) 235 | eventKeys.push(eventKey); 236 | else { 237 | var temp = []; 238 | for (var idx in eventKeys) 239 | if (eventKeys[idx] != eventKey) 240 | temp.push(eventKeys[idx]); 241 | eventKeys = temp; 242 | } 243 | var setRequest = { 244 | Key: EVENT_TITLE_DATA_KEY, 245 | Value: JSON.stringify(eventKeys) 246 | }; 247 | server.SetTitleData(setRequest); 248 | } 249 | ///////////////////////// Define the handlers ///////////////////////// 250 | handlers["GetBaseClassForType"] = GetBaseClassForType; 251 | handlers["CreateCharacter"] = CreateCharacter; 252 | handlers["DeleteCharacter"] = DeleteCharacter; 253 | handlers["SaveProgress"] = SaveProgress; 254 | handlers["RetriveQuestItems"] = RetriveQuestItems; 255 | handlers["SubtractLife"] = SubtractLife; 256 | //# sourceMappingURL=UnicornBattle.js.map -------------------------------------------------------------------------------- /UnicornBattle.ts: -------------------------------------------------------------------------------- 1 | var defaultCatalog = "CharacterClasses"; 2 | var GEM_CURRENCY_CODE = "GM"; 3 | var GOLD_CURRENCY_CODE = "AU"; 4 | var HEART_CURRENCY_CODE = "HT"; 5 | 6 | interface ICreateCharacter { 7 | characterName: string; 8 | catalogCode: string; 9 | } 10 | interface ILCharacterId { 11 | characterId: string; 12 | } 13 | interface ICCharacterId { 14 | CharacterId: string; 15 | } 16 | interface IItemIds { 17 | ItemIds: Array; 18 | } 19 | interface IGetBaseClassForType { 20 | cCode: string; 21 | } 22 | interface ISaveProgress { 23 | CurrentPlayerData: ICharacterInfo; 24 | QuestProgress: IQuestProgress; 25 | LevelRamp: { [key: string]: number }; 26 | PlayerVitals: IPlayerVitals; 27 | } 28 | interface ICharacterInfo { 29 | characterDetails?: ICharacterDetails; 30 | baseClass?: IClassDefinition; 31 | characterData?: ICharacterData; 32 | PlayerVitals?: IPlayerVitals; 33 | } 34 | interface ICharacterData { // here 35 | ClassDetails: IClassDetail; 36 | TotalExp: number; 37 | ExpThisLevel: number; 38 | Health: number; 39 | Mana: number; 40 | Defense: number; 41 | Speed: number; 42 | CharacterLevel: number; 43 | Spell1_Level: number; 44 | Spell2_Level: number; 45 | Spell3_Level: number; 46 | CustomAvatar: string, 47 | } 48 | interface IPlayerVitals { 49 | Health: number; 50 | Mana: number; 51 | Speed: number; 52 | Defense: number; 53 | ActiveStati: Array; 54 | 55 | MaxHealth: number; 56 | MaxMana: number; 57 | MaxSpeed: number; 58 | MaxDefense: number; 59 | 60 | didLevelUp: boolean; 61 | skillSelected: number; 62 | } 63 | interface ISpellStatus { 64 | StatusName: string; 65 | Target: string; 66 | UpgradeReq: string; 67 | StatusDescription: string; 68 | StatModifierCode: string; // prbably need to map to an enum 69 | ModifyAmount: number; 70 | ChanceToApply: number; 71 | Turns: number; 72 | Icon: string; 73 | FX: string; 74 | } 75 | interface IQuestProgress { 76 | XpCollected?: number; 77 | GoldCollected?: number; 78 | } 79 | interface IClassDefinition { 80 | DPLevelBonus: number; 81 | SPLevelBonus: number; 82 | MPLevelBonus: number; 83 | HPLevelBonus: number; 84 | } 85 | interface ICharacterDetails { 86 | CharacterId?: string; 87 | } 88 | interface IClassDetail { 89 | Description: string; 90 | CatalogCode: string; 91 | Icon: string; 92 | Spell1: string; 93 | Spell2: string; 94 | Spell3: string; 95 | BaseHP: number; 96 | BaseMP: number; 97 | BaseDP: number; 98 | BaseSP: number; 99 | HPLevelBonus: number; 100 | MPLevelBonus: number; 101 | DPLevelBonus: number; 102 | SPLevelBonus: number; 103 | Prereq: string; 104 | DisplayStatus: string; 105 | } 106 | interface IAchievementData { 107 | AchievementName: string; 108 | MatchingStatistic: string; 109 | SingleStat: boolean; 110 | Threshold: number; 111 | Icon: string; 112 | } 113 | 114 | ///////////////////////// Cloud Script Handler Functions ///////////////////////// 115 | function CreateCharacter(args: ICreateCharacter): boolean { 116 | var grantItemsRequest: PlayFabServerModels.GrantItemsToUserRequest = { 117 | PlayFabId: currentPlayerId, 118 | CatalogVersion: defaultCatalog, 119 | ItemIds: [args.catalogCode] 120 | }; 121 | server.GrantItemsToUser(grantItemsRequest); 122 | 123 | var grantCharRequest: PlayFabServerModels.GrantCharacterToUserRequest = { 124 | PlayFabId: currentPlayerId, 125 | CharacterName: args.characterName, 126 | CharacterType: args.catalogCode 127 | }; 128 | var result = server.GrantCharacterToUser(grantCharRequest); 129 | InitializeNewCharacterData(result.CharacterId, args.catalogCode); // set up default character data 130 | return true; 131 | } 132 | 133 | function DeleteCharacter(args: ILCharacterId): boolean { 134 | var deleteRequest: PlayFabServerModels.DeleteCharacterFromUserRequest = { 135 | PlayFabId: currentPlayerId, 136 | CharacterId: args.characterId, 137 | SaveCharacterInventory: false 138 | }; 139 | server.DeleteCharacterFromUser(deleteRequest); 140 | return true; 141 | } 142 | 143 | function GetBaseClassForType(args: IGetBaseClassForType): IClassDetail { 144 | var getTitleDataRequest: PlayFabServerModels.GetTitleDataRequest = { Keys: ["Classes"] }; 145 | var result = server.GetTitleData(getTitleDataRequest); 146 | 147 | var classes: { [key: string]: IClassDetail } = JSON.parse(result.Data["Classes"]); 148 | for (var each in classes) 149 | if (classes[each].CatalogCode === args.cCode) 150 | return classes[each]; 151 | return null; 152 | } 153 | 154 | function SaveProgress(args: ISaveProgress): void { 155 | args.CurrentPlayerData = !args.CurrentPlayerData ? {} : args.CurrentPlayerData; 156 | args.QuestProgress = !args.QuestProgress ? {} : args.QuestProgress; 157 | args.LevelRamp = !args.LevelRamp ? {} : args.LevelRamp; 158 | 159 | //check for level up 160 | var baseStats = args.CurrentPlayerData.baseClass; 161 | var characterData = args.CurrentPlayerData.characterData; 162 | var vitals = args.CurrentPlayerData.PlayerVitals; 163 | var questProgress = args.QuestProgress; 164 | var experienceLevel = "" + characterData.CharacterLevel; 165 | var experienceTarget: number = args.LevelRamp[experienceLevel]; // int 166 | 167 | if (vitals.didLevelUp) { 168 | // increment the spell 169 | if (vitals.skillSelected === 0) 170 | characterData.Spell1_Level++; 171 | else if (vitals.skillSelected === 1) 172 | characterData.Spell2_Level++; 173 | else 174 | characterData.Spell3_Level++; 175 | 176 | //Update stats 177 | characterData.CharacterLevel++; 178 | 179 | characterData.Defense += baseStats.DPLevelBonus; 180 | characterData.Speed += baseStats.SPLevelBonus; 181 | characterData.Mana += baseStats.MPLevelBonus; 182 | characterData.Health += baseStats.HPLevelBonus; 183 | characterData.TotalExp += questProgress.XpCollected; 184 | 185 | characterData.ExpThisLevel = characterData.TotalExp - experienceTarget; 186 | } else { 187 | characterData.ExpThisLevel += questProgress.XpCollected; 188 | } 189 | 190 | //check for achievements & offers 191 | EvaluateAchievements(args.CurrentPlayerData.characterDetails.CharacterId); 192 | 193 | // API params 194 | var updateDataRequest: PlayFabServerModels.UpdateCharacterDataRequest = { 195 | PlayFabId: currentPlayerId, 196 | CharacterId: args.CurrentPlayerData.characterDetails.CharacterId, 197 | Data: { CharacterData: JSON.stringify(characterData) }, 198 | Permission: "Public" 199 | }; 200 | 201 | server.UpdateCharacterReadOnlyData(updateDataRequest); 202 | 203 | // set up Gold VC 204 | var addVcRequest: PlayFabServerModels.AddUserVirtualCurrencyRequest = { 205 | PlayFabId: currentPlayerId, 206 | VirtualCurrency: GOLD_CURRENCY_CODE, 207 | Amount: questProgress.GoldCollected 208 | }; 209 | server.AddUserVirtualCurrency(addVcRequest); 210 | } 211 | 212 | function RetriveQuestItems(args: IItemIds): string { 213 | var grantRequest: PlayFabServerModels.GrantItemsToUserRequest = { 214 | PlayFabId: currentPlayerId, 215 | ItemIds: args.ItemIds 216 | }; 217 | var response = server.GrantItemsToUser(grantRequest); 218 | return JSON.stringify(response.ItemGrantResults); 219 | } 220 | 221 | function SubtractLife(): PlayFabServerModels.ModifyUserVirtualCurrencyResult { 222 | var subtractVcRequest: PlayFabServerModels.SubtractUserVirtualCurrencyRequest = { 223 | PlayFabId: currentPlayerId, 224 | VirtualCurrency: HEART_CURRENCY_CODE, 225 | Amount: 1 226 | }; 227 | return server.SubtractUserVirtualCurrency(subtractVcRequest); 228 | } 229 | 230 | function EnableValentinesEvent(): void { 231 | SetEventActive("evalentine", true); 232 | } 233 | 234 | function DisableValentinesEvent(): void { 235 | SetEventActive("evalentine", false); 236 | } 237 | 238 | function EnablePresEvent(): void { 239 | SetEventActive("epresident", true); 240 | } 241 | 242 | function DisablePresEvent(): void { 243 | SetEventActive("epresident", false); 244 | } 245 | 246 | ///////////////////////// HELPER FUNCTIONS (NOT DIRECTLY CALLABLE FROM THE CLIENT) ///////////////////////// 247 | function InitializeNewCharacterData(characterId: string, catalogItemId: string): void { 248 | var cDetails: IClassDetail = GetBaseClassForType({ cCode: catalogItemId }); 249 | 250 | // default character properties 251 | var CharacterData: ICharacterData = { 252 | ClassDetails: cDetails, 253 | TotalExp: 0, 254 | ExpThisLevel: 0, 255 | Health: cDetails.BaseHP, 256 | Mana: cDetails.BaseMP, 257 | Defense: cDetails.BaseDP, 258 | Speed: cDetails.BaseSP, 259 | CharacterLevel: 1, 260 | Spell1_Level: 0, 261 | Spell2_Level: 0, 262 | Spell3_Level: 0, 263 | CustomAvatar: null 264 | }; 265 | 266 | // Char Data 267 | var updateDataRequest: PlayFabServerModels.UpdateCharacterDataRequest = { 268 | PlayFabId: currentPlayerId, 269 | CharacterId: characterId, 270 | Data: { CharacterData: JSON.stringify(CharacterData) }, 271 | Permission: "Public" 272 | }; 273 | server.UpdateCharacterReadOnlyData(updateDataRequest); 274 | 275 | // set up Heart VC 276 | var vcHeartRequest: PlayFabServerModels.AddUserVirtualCurrencyRequest = { 277 | PlayFabId: currentPlayerId, 278 | VirtualCurrency: HEART_CURRENCY_CODE, 279 | Amount: 0 280 | }; 281 | server.AddUserVirtualCurrency(vcHeartRequest); 282 | 283 | // set up Gold VC 284 | var vcGoldRequest: PlayFabServerModels.AddUserVirtualCurrencyRequest = { 285 | PlayFabId: currentPlayerId, 286 | VirtualCurrency: GOLD_CURRENCY_CODE, 287 | Amount: 0 288 | }; 289 | server.AddUserVirtualCurrency(vcGoldRequest); 290 | 291 | // set up Gem VC 292 | var vcGemRequest: PlayFabServerModels.AddUserVirtualCurrencyRequest = { 293 | PlayFabId: currentPlayerId, 294 | VirtualCurrency: GEM_CURRENCY_CODE, 295 | Amount: 0 296 | }; 297 | server.AddUserVirtualCurrency(vcGemRequest); 298 | } 299 | 300 | function HasAchievement(achievement: IAchievementData, playerStats: { [key: string]: number }): boolean { 301 | if (achievement.SingleStat) { 302 | for (var stat in playerStats) 303 | if (playerStats.hasOwnProperty(stat) && stat.indexOf(achievement.MatchingStatistic) > -1 304 | && playerStats[stat] >= achievement.Threshold) 305 | // Stat found and exceeds the achievement threshold 306 | return true; 307 | } else { 308 | // process aggregate stats 309 | var statTotal = 0; 310 | for (var stat in playerStats) 311 | if (playerStats.hasOwnProperty(stat) && stat.indexOf(achievement.MatchingStatistic) > -1) 312 | statTotal += playerStats[stat]; 313 | 314 | if (statTotal >= achievement.Threshold) 315 | return true; // sum of stats found exceeds the achievement threshold 316 | } 317 | 318 | return false; 319 | } 320 | 321 | function EvaluateAchievements(characterId: string): void { 322 | // get the achievement thresholds set by TitleData 323 | var getTitleAchievementsRequest = { Keys: ["Achievements"] }; 324 | var titleDataResult = server.GetTitleData(getTitleAchievementsRequest); 325 | var serverAchievements: { [key: string]: IAchievementData }; 326 | if (titleDataResult.Data.hasOwnProperty("Achievements")) 327 | serverAchievements = JSON.parse(titleDataResult.Data["Achievements"]); 328 | else 329 | throw "Achievements not found on Server. Check TitleData[\"Achievements\"]"; 330 | 331 | // get the character stats 332 | var statsRequest: PlayFabServerModels.GetCharacterStatisticsRequest = { 333 | PlayFabId: currentPlayerId, 334 | CharacterId: characterId, 335 | }; 336 | var statsResult = server.GetCharacterStatistics(statsRequest); 337 | var charStats = statsResult.CharacterStatistics; 338 | 339 | // get the unlocked stats for the character 340 | var getCharDataRequest = { 341 | PlayFabId: currentPlayerId, 342 | CharacterId: characterId, 343 | Keys: ["Achievements"] 344 | }; 345 | var charDataResult = server.GetCharacterReadOnlyData(getCharDataRequest); 346 | var awardedAchievements: Array; 347 | if (charDataResult.Data.hasOwnProperty("Achievements")) 348 | awardedAchievements = JSON.parse(charDataResult.Data["Achievements"].Value); 349 | else 350 | awardedAchievements = []; // no achievements 351 | 352 | // TODO -- need to track new achievements and toss them over to the offer evaluator 353 | var arrayCount = awardedAchievements.length; 354 | for (var achievementName in serverAchievements) 355 | // if the player does not already have the achievement, evaluate progress 356 | if (awardedAchievements.indexOf(achievementName) === -1 && HasAchievement(serverAchievements[achievementName], charStats)) 357 | awardedAchievements.push(achievementName); 358 | 359 | // if we added some new achievements, save details to character data 360 | if (arrayCount < awardedAchievements.length) { 361 | var updateCharDataRequest: PlayFabServerModels.UpdateCharacterDataRequest = { 362 | PlayFabId: currentPlayerId, 363 | CharacterId: characterId, 364 | Data: { Achievements: JSON.stringify(awardedAchievements) }, 365 | Permission: "Public" 366 | }; 367 | server.UpdateCharacterReadOnlyData(updateCharDataRequest); 368 | } 369 | } 370 | 371 | var EVENT_TITLE_DATA_KEY: string = "ActiveEventKeys"; 372 | function SetEventActive(eventKey: string, isActive: boolean): void { 373 | var getRequest: PlayFabServerModels.GetTitleDataRequest = { Keys: [EVENT_TITLE_DATA_KEY] }; 374 | var serverData = server.GetTitleData(getRequest); 375 | var eventKeys: Array = JSON.parse(serverData.Data[EVENT_TITLE_DATA_KEY]); 376 | if (isActive) 377 | eventKeys.push(eventKey); 378 | else { 379 | var temp: Array = []; 380 | for (var idx in eventKeys) 381 | if (eventKeys[idx] != eventKey) 382 | temp.push(eventKeys[idx]); 383 | eventKeys = temp; 384 | } 385 | 386 | var setRequest: PlayFabServerModels.SetTitleDataRequest = { 387 | Key: EVENT_TITLE_DATA_KEY, 388 | Value: JSON.stringify(eventKeys) 389 | }; 390 | server.SetTitleData(setRequest); 391 | } 392 | 393 | ///////////////////////// Define the handlers ///////////////////////// 394 | handlers["GetBaseClassForType"] = GetBaseClassForType; 395 | handlers["CreateCharacter"] = CreateCharacter; 396 | handlers["DeleteCharacter"] = DeleteCharacter; 397 | handlers["SaveProgress"] = SaveProgress; 398 | handlers["RetriveQuestItems"] = RetriveQuestItems; 399 | handlers["SubtractLife"] = SubtractLife; 400 | -------------------------------------------------------------------------------- /genConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "buildFlags": "requiresoptionalvaluesupport", 4 | "templateFolder": "SdkTestingCloudScript", 5 | "versionKey": "sdktestingcloudscript" 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playfab-cloudscriptexample", 3 | "version": "0.0.220509", 4 | "description": "PlayFab CloudScriptExample", 5 | "main": "ExampleCloudScript.js", 6 | "author": { 7 | "name": "PlayFab", 8 | "email": "helloplayfab@microsoft.com" 9 | } 10 | } 11 | --------------------------------------------------------------------------------