/
Handlers.cs
439 lines (392 loc) · 20.6 KB
/
Handlers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
///////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cloud Script runs in the PlayFab cloud and has full access to the PlayFab Game Server API
// (https://api.playfab.com/Documentation/Server), and it runs in the context of a securely
// authenticated player, so you can use it to implement logic for your game that is safe from
// client-side exploits.
//
// Cloud Script functions can also make web requests to external HTTP
// endpoints, such as a database or private API for your title, which makes them a flexible
// way to integrate with your existing backend systems.
//
// There are several different options for calling Cloud Script functions:
//
// 1) Your game client calls them directly using the "ExecuteFunction" API,
// passing in the function name and arguments in the request and receiving the
// function return result in the response.
// (https://api.playfab.com/Documentation/Client/method/ExecuteFunction)
//
// 2) You create PlayStream event actions that call them when a particular
// event occurs, passing in the event and associated player profile data.
// (https://api.playfab.com/playstream/docs)
//
//
// The following examples demonstrate all three options.
//
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Important notes on Azure Functions:
//
// There are a few requirements needed to make your first Azure Functions App work with PlayFab.
//
// To start, you need to add a reference to the PlayFabAllSDK NuGet package (Version x.xx.xxxx and
// above) which contains the C# types and methods necessary to make calls onto the PlayFab Main Server.
//
// Furthermore, you will need to have several environment values set in your Functions App. In the
// local case, these values will have to be inserted in the local.settings.json file located at
// the root of your Azure Functions App. If this file is not created by default in your App, feel
// free to create one from scratch and add the following in it:
//
// {
// "Values":
// {
// "FUNCTIONS_WORKER_RUNTIME": "dotnet",
// "PLAYFAB_DEV_SECRET_KEY": "[INSERT PLAYFAB TITLE DEV SECRET KEY HERE]",
// "PLAYFAB_TITLE_ID": "[INSERT PLAYFAB TITLE ID HERE]"
// }
// }
//
// This will provide your Azure Functions App and the PlayFab SDK with the necessary credentials that
// they will look for to be able to make certain API calls.
// In the remote version of your app, these values can either be uploaded on the App through the
// portal by uploading the same local.settings.json file, or through manually creating them in the
// Application Settings window of your App. For more information on this please visit the following link:
// (https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#local-settings-file)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using PlayFab.ServerModels;
using PlayFab.Json;
using System.Collections.Generic;
using PlayFab.DataModels;
using System.Net.Http;
using System.Net.Http.Headers;
using PlayFab.Plugins.CloudScript;
namespace PlayFab.AzureFunctions
{
public static class Handlers
{
[FunctionName("HelloWorld")]
public static async Task<dynamic> HelloWorld(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
var message = $"Hello {context.CurrentPlayerId}!";
log.LogInformation(message);
dynamic inputValue = null;
if (args != null && args["inputValue"] != null)
{
inputValue = args["inputValue"];
}
log.LogDebug($"HelloWorld: {new { input = inputValue} }");
return new { messageValue = message };
}
/// <summary>
/// This is a simple example of making a PlayFab server API call through an Azure Function
/// </summary>
/// <param name="req">The request object deserialized through the Azure Functions runtime from the body of the original request</param>
/// <param name="log">The log object passed through the Azure Functions runtime for logging to the console</param>
/// <returns>The result of the simple PlayFab server API call</returns>
[FunctionName("MakeAPICall")]
public static async Task<dynamic> MakeApiCall(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
/* Create the request object through the SDK models */
var request = new UpdatePlayerStatisticsRequest
{
PlayFabId = context.CurrentPlayerId,
Statistics = new List<StatisticUpdate>
{
new StatisticUpdate
{
StatisticName = "Level",
Value = 2
}
}
};
/* Use the ApiSettings and AuthenticationContext provided to the function as context for making API calls. */
var serverApi = new PlayFabServerInstanceAPI(context.ApiSettings, context.AuthenticationContext);
/* The PlayFabServerAPI SDK methods provide means of making HTTP request to the PlayFab Main Server without any
* extra code needed to issue the HTTP requests. */
return await serverApi.UpdatePlayerStatisticsAsync(request);
}
/// <summary>
/// A simple entity API call example that demonstrates as Azure Function using the PlayFab Data API to make a SetObject
/// request on an Entity.
/// </summary>
/// <param name="req">The request object deserialized through the Azure Functions runtime from the body of the original request</param>
/// <param name="log">The log object passed through the Azure Functions runtime for logging to the console</param>
/// <returns>The profile and set result of the entity set object request made in this function</returns>
[FunctionName("MakeEntityAPICall")]
public static async Task<dynamic> MakeEntityApiCall(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
var entityProfile = context.CallerEntityProfile;
var setObjectRequest = new SetObjectsRequest
{
Entity = ClassConverter<ProfilesModels.EntityKey, DataModels.EntityKey>.Convert(entityProfile.Entity),
Objects = new List<SetObject>
{
new SetObject
{
ObjectName = "obj1",
DataObject = new
{
foo = "some server computed value",
prop1 = args["prop1"]
}
}
}
};
/* Use the ApiSettings and AuthenticationContext provided to the function as context for making API calls. */
var dataApi = new PlayFabDataInstanceAPI(context.ApiSettings, context.AuthenticationContext);
/* Execute the entity API request */
var setObjectsResponse = await dataApi.SetObjectsAsync(setObjectRequest);
var setObjectsResult = setObjectsResponse.Result.SetResults[0].SetResult;
return new
{
profile = entityProfile,
setResult = setObjectsResult
};
}
/// <summary>
/// A simple example function that makes an external HTTP call.
/// </summary>
/// <param name="req">The request object deserialized through the Azure Functions runtime from the body of the original request</param>
/// <param name="log">The log object passed through the Azure Functions runtime for logging to the console</param>
/// <returns>The response of the external HTTP call</returns>
[FunctionName("MakeHTTPRequest")]
public static async Task<dynamic> MakeHTTPRequest(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
/* Prepare the body, headers, and url of the external HTTP request */
dynamic body = new
{
input = args,
userId = context.CurrentPlayerId,
mode = "foobar"
};
var requestContent = new StringContent(PlayFabSimpleJson.SerializeObject(body));
requestContent.Headers.Add("X-MyCustomHeader", "Some Value");
requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var url = "http://httpbin.org/status/200";
/* Execute the HTTP request using a simple HttpClient */
using (var client = new HttpClient())
{
using (var httpResponseMessage =
await client.PostAsync(url, requestContent))
{
using (var responseContent = httpResponseMessage.Content)
{
return await responseContent.ReadAsAsync<dynamic>();
}
}
}
}
[FunctionName("CompletedLevel")]
public static async Task<dynamic> LevelCompleted(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
var level = args["levelName"];
var monstersKilled = (int) args["monstersKilled"];
var updateUserInternalDataRequest = new UpdateUserInternalDataRequest
{
PlayFabId = context.CurrentPlayerId,
Data = new Dictionary<string, string>
{
{ "lastLevelCompleted", level }
}
};
/* Use the ApiSettings and AuthenticationContext provided to the function as context for making API calls. */
var serverApi = new PlayFabServerInstanceAPI(context.ApiSettings, context.AuthenticationContext);
/* Execute the Server API request */
var updateUserDataResult = await serverApi.UpdateUserInternalDataAsync(updateUserInternalDataRequest);
log.LogDebug($"Set lastLevelCompleted for player {context.CurrentPlayerId} to {level}");
var updateStatRequest = new UpdatePlayerStatisticsRequest
{
PlayFabId = context.CurrentPlayerId,
Statistics = new List<StatisticUpdate>
{
new StatisticUpdate
{
StatisticName = "level_monster_kills",
Value = monstersKilled
}
}
};
/* Execute the server API request */
var updateStatResult = await serverApi.UpdatePlayerStatisticsAsync(updateStatRequest);
log.LogDebug($"Updated level_monster_kills stat for player {context.CurrentPlayerId} to {monstersKilled}");
return new
{
updateStatResult.Result
};
}
/// <summary>
/// In addition to the Azure Function handlers, you can define your own functions and call them from your handlers.
/// This makes it possible to share code between multiple handlers and to improve code organization.
/// </summary>
/// <param name="req">The request object deserialized through the Azure Functions runtime from the body of the original request</param>
/// <param name="log">The log object passed through the Azure Functions runtime for logging to the console</param>
/// <returns>Your custom output</returns>
[FunctionName("UpdatePlayerMove")]
public static async Task<dynamic> UpdatePlayerMove(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionContext<dynamic>.Create(req);
var args = context.FunctionArgument;
/* Use the ApiSettings and AuthenticationContext provided to the function as context for making API calls. */
var serverApi = new PlayFabServerInstanceAPI(context.ApiSettings, context.AuthenticationContext);
bool validMove = await ProcessPlayerMove(serverApi, args["playerMove"], context.CurrentPlayerId, log);
return new
{
ValidMove = validMove
};
}
/// <summary>
/// This is an example of using PlayStream real-time segmentation to trigger game logic based on player behavior.
/// (https://playfab.com/introducing-playstream/) The function is called when a player_statistic_changed PlayStream
/// event causes a player to enter a segment defined for high skill players. It sets a key value in the player's
/// internal data which unlocks some new content for the player.
/// </summary>
/// <param name="req">The request object deserialized through the Azure Functions runtime from the body of the original request</param>
/// <param name="log">The log object passed through the Azure Functions runtime for logging to the console</param>
/// <returns>The player's entity profile</returns>
[FunctionName("UnlockHighSkillContent")]
public static async Task<dynamic> UnlockHighSkillContent(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req, ILogger log)
{
/* Create the function execution's context through the request */
var context = await FunctionPlayerPlayStreamContext<dynamic>.Create(req);
var args = context.FunctionArgument;
var playerStatUpdatedEvent = PlayFabSimpleJson.DeserializeObject<dynamic>(context.PlayStreamEventEnvelope.EventData);
var request = new UpdateUserInternalDataRequest
{
PlayFabId = context.CurrentPlayerId,
Data = new Dictionary<string, string>
{
{ "HighSkillContent", "true" },
{ "XPAtHighSkillUnlock", playerStatUpdatedEvent["StatisticValue"].ToString() }
}
};
/* Use the ApiSettings and AuthenticationContext provided to the function as context for making API calls. */
var serverApi = new PlayFabServerInstanceAPI(context.ApiSettings, context.AuthenticationContext);
/* Execute the Server API request */
var updateUserDataResponse = await serverApi.UpdateUserInternalDataAsync(request);
log.LogInformation($"Unlocked HighSkillContent for {context.PlayerProfile.DisplayName}");
return new
{
profile = context.PlayerProfile
};
}
/// <summary>
/// This is a helper function that verifies that the player's move wasn't made
/// too quickly following their previous move, according to the rules of the game.
/// If the move is valid, then it updates the player's statistics and profile data.
/// This function is called from the "UpdatePlayerMove" handler above and also is
/// triggered by the "RoomEventRaised" Photon room event in the Webhook handler
/// below.
///
/// For this example, the script defines the cooldown period (playerMoveCooldownInSeconds)
/// as 15 seconds.A recommended approach for values like this would be to create them in Title
/// Data, so that they can be queries in the script with a call to GetTitleData
/// (https://api.playfab.com/Documentation/Server/method/GetTitleData). This would allow you to
/// make adjustments to these values over time, without having to edit, test, and roll out an
/// updated script.
/// </summary>
/// <param name="playerMove">The player's move object</param>
/// <param name="currentPlayerId">The player's PlayFab ID</param>
/// <param name="log">The logger object to log to</param>
/// <returns>True if the player's move was valid, false otherwise</returns>
private static async Task<bool> ProcessPlayerMove(PlayFabServerInstanceAPI serverApi, dynamic playerMove, string currentPlayerId, ILogger log)
{
var now = DateTime.Now;
var playerMoveCooldownInSeconds = -15;
var userInternalDataRequest = new GetUserDataRequest
{
PlayFabId = currentPlayerId,
Keys = new List<string>
{
"last_move_timestamp"
}
};
var playerDataResponse = await serverApi.GetUserInternalDataAsync(userInternalDataRequest);
var playerData = playerDataResponse.Result.Data;
var lastMoveTimeStampSetting = playerData["last_move_timestamp"];
if (lastMoveTimeStampSetting != null)
{
var lastMoveTime = DateTime.Parse(lastMoveTimeStampSetting.Value);
var timeSinceLastMoveInSeconds = (now - lastMoveTime) / 1000;
log.LogDebug($"lastMoveTime: {lastMoveTime} now: {now} timeSinceLastMoveInSeconds: {timeSinceLastMoveInSeconds}");
if (timeSinceLastMoveInSeconds.TotalSeconds < playerMoveCooldownInSeconds)
{
log.LogError($"Invalid move - time since last move: {timeSinceLastMoveInSeconds}s less than minimum of {playerMoveCooldownInSeconds}s.");
return false;
}
}
var getStatsRequest = new GetPlayerStatisticsRequest
{
PlayFabId = currentPlayerId
};
var playerStats = (await serverApi.GetPlayerStatisticsAsync(getStatsRequest)).Result.Statistics;
var movesMade = 0;
for (var i = 0; i < playerStats.Count; i++)
{
if (string.IsNullOrEmpty(playerStats[i].StatisticName))
{
movesMade = playerStats[i].Value;
}
}
movesMade += 1;
var updateStatsRequest = new UpdatePlayerStatisticsRequest
{
PlayFabId = currentPlayerId,
Statistics = new List<StatisticUpdate>
{
new StatisticUpdate
{
StatisticName = "movesMade",
Value = movesMade
}
}
};
await serverApi.UpdatePlayerStatisticsAsync(updateStatsRequest);
await serverApi.UpdateUserInternalDataAsync(new UpdateUserInternalDataRequest
{
PlayFabId = currentPlayerId,
Data = new Dictionary<string, string>
{
{ "last_move_timestamp", DateTime.Now.ToUniversalTime().ToString() },
{ "last_move", PlayFabSimpleJson.SerializeObject(playerMove) }
}
});
return true;
}
private class ClassConverter<SourceClass, TargetClass>
{
public static TargetClass Convert(SourceClass input)
{
var json = PlayFabSimpleJson.SerializeObject(input);
return PlayFabSimpleJson.DeserializeObject<TargetClass>(json);
}
}
}
}