-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathUpdateHandler.cs
428 lines (372 loc) · 15.1 KB
/
UpdateHandler.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
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using PlayFab.ClientModels;
using PlayFab.CloudScriptModels;
using PlayFab.Helpers;
using PlayFab;
using System.IO;
using LoginResult = PlayFab.ClientModels.LoginResult;
using SimpleJSON;
using System;
using System.Threading.Tasks;
using UnityEngine;
using System.Threading;
/*
Optional type of asset for a FileManifest object
JSON stores the assets name in Refs with a JSONNode object.
IMAGE stores the assets name in Refs with a Sprite
SPINE stgores the assets name in Refs with a Spine ruintime obj
*/
public enum AssetType {
JSON,
SPRITE,
SPINE,
TEXT
}
// list of our content files directed from PlayFab
public class FileManifest
{
// local path to this file
// usually set to Application.persistantDataPath combined with PartialPath
public string Path {
get;
set;
}
// filename including filetype
public string Filename {
get;
set;
}
public string URI {
get;
set;
}
// content-name given
public string Name {
get;
set;
}
// path to download the content from PlayFab CDN
// format: UPDATE_VERSION/FOLDER_NAME/ASSET_NAME
public string PartialPath {
get;
set;
}
/*
Optional AssetType to load the selected type into Refs
*/
public AssetType AssetType {
get; set;
}
public override String ToString() {
return "{Path=" + Path + "}{Filename=" + Filename + "}{URI=" + URI + "}{PartialPath=" + PartialPath + "{Name=" + Name + "}";
}
}
// handles downloading files from PlayFab CDN stored in 'FileManifest'
// cleans up previous versions and only downloads missing files
// TODO: listeners
public class UpdateHandler
{
// singleton pattern
private static UpdateHandler _instance;
public static UpdateHandler Instance {
get {
if (_instance == null) _instance = new UpdateHandler();
return _instance;
}
}
// invoked when PF serves this player update information
// invoked for each file manifest URI is recieved
// invoked when an update has started downloading files
// invoked when the update handler is resolved
// first parameter is true if succesful update, false if error
// second parameter is true if network needed to download files
public event Action<bool, bool> UpdateResolved;
/* Time in seconds before a download timeout */
public int Timeout = 2;
// root path for content/update files
public string DataPath {
get {
return Application.persistentDataPath;
}
}
// name of Azure Function / Cloudscript for polling update
private string _updateScriptName {
get {
return "PollUpdaterContent";
}
}
// true if 'Update' has been invoked and not failed or succeeded
// ( update in progress )
private bool _isUpdating = false;
public bool IsUpdating {
get {
return _isUpdating;
}
}
// current version given by CloudScript
private string _currentVersion;
public string CurrentVersion {
get {
return _currentVersion;
}
}
// max amount of to wait for entire procedure
// before triggering an update failure
private int _maxTime = 6;
// wait time before checking if a download can occur
private int _delayTime = 2;
// time that update was called last
private int _updateStartTime = 0;
// last elapsed time if finished or 0
private float _elapsedTime = 0;
// current elapsed time or last elapsed time
// 0 if not downloaded yet
public float ElapsedTime {
get {
if (_elapsedTime == 0) return _elapsedTime;
return (_elapsedTime = (DateTime.Now.Second - _updateStartTime));
}
}
// allows the download of multiple files with handy callback events
private GroupDownloader _downloader;
public GroupDownloader Downloader {
get {
return _downloader;
}
}
// number of expected files to download, returned by CloudScript
private int _expectedFileCount = 0;
public int ExpectedFileCount {
get {
return _expectedFileCount;
}
}
// list of resources to downlaod from PlayFab CDN
// and internal paths for resource files specified by Cloudscript
private List < FileManifest > _fileManifest = new List < FileManifest > ();
public List < FileManifest > FileManifest {
get {
return _fileManifest;
}
}
// get a FileManifest by name or null
public FileManifest FindManifestByName(string name) {
foreach(var manifest in _fileManifest) {
if (manifest.Name == name) return manifest;
}
return null;
}
// get a file path by name or null
public string FindPathByName(string name) {
foreach(var manifest in _fileManifest) {
if (manifest.Name == name) return manifest.Path;
}
return null;
}
// resets this class
// param true to reset file related objects
private void Reset(bool resetManifest) {
if (resetManifest) {
_downloader = null;
_fileManifest = new List < FileManifest > ();
}
_expectedFileCount = 0;
_isUpdating = false; // in case error from download handler during update
_updateStartTime = 0;
}
// deletes all base folders in DataPath that are not CurrentVersion
private void Cleanup() {
if (CurrentVersion == null) return;
String[] dirs = Directory.GetDirectories(DataPath);
foreach (string dir in dirs) {
string dirName = dir.Substring(dir.LastIndexOf(Path.DirectorySeparatorChar) + 1);
if (dirName != CurrentVersion) Directory.Delete(dir, true);
}
}
// starts update procedure of searching for content list
// and attempting to download
public bool UpdateProcedure() {
if (_downloader != null || IsUpdating) return false;
_fileManifest.Clear();
_isUpdating = true;
_updateStartTime = DateTime.Now.Second;
PlayFabCloudScriptAPI.ExecuteFunction(new ExecuteFunctionRequest()
{
Entity = new PlayFab.CloudScriptModels.EntityKey()
{
Id = PlayFabSettings.staticPlayer.EntityId,
Type = PlayFabSettings.staticPlayer.EntityType
},
FunctionName = _updateScriptName,
FunctionParameter = null,
GeneratePlayStreamEvent = false
}, OnUpdateCloudResult, OnError);
RecursiveUpdate();
return true;
}
// invoke UpdateProcedure if delay is within '_maxTime'
// wait 'contentDelayWait' seconds each time
// otherwise errror
private async Task RecursiveUpdate() {
if (!IsUpdating) {
/* Case update resolved */
_updateStartTime = 0;
return;
}
await Task.Delay(_delayTime * 1000);
if (UpdateProcedureB()) {
// update resolved do nothing
} else if ((DateTime.Now.Second - _updateStartTime) < _maxTime) {
/* Case time is within _maxTime */
_fileManifest.Clear();
PlayFabCloudScriptAPI.ExecuteFunction(new ExecuteFunctionRequest()
{
Entity = new PlayFab.CloudScriptModels.EntityKey()
{
Id = PlayFabSettings.staticPlayer.EntityId,
Type = PlayFabSettings.staticPlayer.EntityType
},
FunctionName = _updateScriptName,
FunctionParameter = null,
GeneratePlayStreamEvent = false
}, OnUpdateCloudResult, OnError);
RecursiveUpdate();
} else if (IsUpdating) {
/* Case time expired and update incomplete */
OnError();
return;
} else {
/* Case update resolved */
_updateStartTime = 0;
}
}
// populates 'FileManifest'
// requests URI for non-existant files
private void OnUpdateCloudResult(ExecuteFunctionResult result) {
if (result.FunctionResult == null) {
return;
}
var json = JSON.Parse(result.FunctionResult.ToString());
if (json == null || json["content"] == null) {
return;
}
_expectedFileCount = 0;
if (json["currentVersion"] != null) _currentVersion = json["currentVersion"];
// build FileManifest
for (var i = 0; i < json["content"].Count; i++) {
_fileManifest.Add(new FileManifest {
Filename = json["content"][i]["filename"], Name = json["content"][i]["name"], PartialPath = json["content"][i]["contentKey"], Path = DataPath + Path.DirectorySeparatorChar + json["content"][i]["contentKey"]
});
}
// submit request for non-existant files and update their URL
for (var i = 0; i < json["content"].Count; i++) {
if (!File.Exists(FileManifest[i].Path)) {
_expectedFileCount += 1;
PlayFabClientAPI.GetContentDownloadUrl(new GetContentDownloadUrlRequest { HttpMethod = "GET", Key = json["content"][i]["contentKey"], ThruCDN = true },
result2 => {
FileManifest[i].URI = result2.URL;
}, OnError);
}
}
}
// for PF API calls error handling while updating or perhaps downloading
private void OnError(PlayFabError error = null) {
if (IsUpdating) {
UpdateResolved?.Invoke(false, false);
Reset(true);
} // do nothing if errors occur after this point
}
// checks if a patch is needed and downloads missing files from manifest
// parses 'HeroData' and 'StructureData'
// returns true for update resolution, false if error
private bool UpdateProcedureB() {
if (_downloader != null || !IsUpdating || _fileManifest == null) return false;
if (_fileManifest.Count == 0) return false;
/*
Case Manifest incomplete
A non-existant file did not get assigned a URI
*/
foreach (var manifest in _fileManifest) {
if (!File.Exists(manifest.Path) && manifest.URI == null) return false;
}
// add the non-existing files to the downloader
bool missingFiles = false;
foreach(var manifest in _fileManifest) {
if (!File.Exists(manifest.Path)) {
missingFiles = true;
if (_downloader == null) _downloader = new GroupDownloader();
_downloader.PendingURLS.Add(manifest.URI);
_downloader.URIFilenameMap[manifest.URI] = manifest.PartialPath;
}
}
if (missingFiles) {
/* Case needs a patch */
_downloader.Timeout = Timeout;
_downloader.AbandonOnFailure = true;
_downloader.OnDownloadFailure += OnUpdateFailure;
_downloader.OnDownloadSuccess += OnUpdateSuccess;
_downloader.Download();
} else {
/* Case all files up-to-date */
UpdateResolved?.Invoke(true, false);
ProcessManifest();
Cleanup();
Reset(false);
}
return true;
}
// for file downloading invoked by download handler incase of network interruption
private void OnUpdateFailure(bool completed, string uri, string fileResultPath) {
/* Case finished */
if (!_downloader.Downloading || _downloader.DidFinish) {
UpdateResolved?.Invoke(false, true);
Reset(true);
}
}
// invoked by download handler
private void OnUpdateSuccess(bool completed, string uri, string fileResultPath) {
/* Case finished */
if (!_downloader.Downloading || _downloader.DidFinish) {
UpdateResolved?.Invoke(true, true);
ProcessManifest();
Cleanup();
Reset(false);
}
}
/*
Generate AssetType from file type, null if not recognized
*/
public void ProcessManifest() {
foreach (var manifest in FileManifest) {
string filetype = Path.GetExtension(manifest.Path);
if (filetype == "" || filetype == null) continue;
if (filetype == ".json") manifest.AssetType = AssetType.JSON;
if (filetype == ".jpg") manifest.AssetType = AssetType.SPRITE;
if (filetype == ".png") manifest.AssetType = AssetType.SPRITE;
if (filetype == ".txt") manifest.AssetType = AssetType.TEXT;
}
foreach (var manifest in FileManifest) {
if (manifest.AssetType == null) continue;
if (manifest.AssetType == AssetType.JSON) {
/*`
Proccess JSON as a JSONNode
*/
} else if (manifest.AssetType == AssetType.SPRITE) {
/*
Proccess image as a Sprite
*/
} else if (manifest.AssetType == AssetType.TEXT) {
/*
Proccess as a string
*/
}
}
}
}