├── .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 |
--------------------------------------------------------------------------------