/** * Storage System takes care loading and linking components and dependencies * @param graphics * @param apiSystem GameLib.API.System * @param apiUrl * @param token * @param apiUploadUrl * @param onImageLoaded * @param onImageProgress * @param onImageError * @param onComponentLoaded * @param onComponentProgress * @param onComponentError * @constructor */ GameLib.System.Storage = function( graphics, apiSystem, apiUrl, token, apiUploadUrl, onImageLoaded, onImageProgress, onImageError, onComponentLoaded, onComponentProgress, onComponentError ) { this.graphics = graphics; this.graphics.isNotThreeThrow(); GameLib.System.call( this, apiSystem ); if (GameLib.Utils.UndefinedOrNull(apiUrl)) { console.warn('Need an API URL for a storage system'); apiUrl = ''; } this.apiUrl = apiUrl; if (GameLib.Utils.UndefinedOrNull(token)) { token = null; } this.token = token; if (GameLib.Utils.UndefinedOrNull(apiUploadUrl)) { console.warn('Need an API Upload URL for a storage system'); apiUploadUrl = ''; } this.apiUploadUrl = apiUploadUrl; if (GameLib.Utils.UndefinedOrNull(onImageLoaded)) { onImageLoaded = null; } this.onImageLoaded = onImageLoaded; if (GameLib.Utils.UndefinedOrNull(onImageProgress)) { onImageProgress = null; } this.onImageProgress = onImageProgress; if (GameLib.Utils.UndefinedOrNull(onImageError)) { onImageError = null; } this.onImageError = onImageError; if (GameLib.Utils.UndefinedOrNull(onComponentLoaded)) { onComponentLoaded = null; } this.onComponentLoaded = onComponentLoaded; if (GameLib.Utils.UndefinedOrNull(onComponentProgress)) { onComponentProgress = null; } this.onComponentProgress = onComponentProgress; if (GameLib.Utils.UndefinedOrNull(onComponentError)) { onComponentError = null; } this.onComponentError = onComponentError; this.loginSubscription = null; this.saveSubscription = null; this.loadSubscription = null; this.loadImageSubscription = null; }; GameLib.System.Storage.prototype = Object.create(GameLib.System.prototype); GameLib.System.Storage.prototype.constructor = GameLib.System.Storage; GameLib.System.Storage.prototype.start = function() { this.loginSubscription = this.subscribe( GameLib.Event.LOGGED_IN, function(data) { this.token = data.token; } ); this.saveSubscription = this.subscribe( GameLib.Event.SAVE_COMPONENT, this.save ); this.loadSubscription = this.subscribe( GameLib.Event.LOAD_COMPONENT, this.load ); this.loadImageSubscription = this.subscribe( GameLib.Event.LOAD_IMAGE, this.loadImage ); }; /** * 'Saves' data to baseURL */ GameLib.System.Storage.prototype.save = function(data) { if (typeof XMLHttpRequest === 'undefined') { console.log('Implement server side save here'); return; } var xhr = new XMLHttpRequest(); xhr.open( 'POST', this.apiUrl + '/component/create' ); xhr.setRequestHeader("Accept", "application/json"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function () { if (this.readyState === 4) { try { var response = JSON.parse(this.responseText) } catch (error) { GameLib.Event.Emit( GameLib.Event.SAVE_COMPONENT_ERROR, { message: this.responseText } ) } if (response.result === 'success') { GameLib.Event.Emit( GameLib.Event.COMPONENT_SAVED, { message: response.message || 'Successfully saved the component' } ) } else { GameLib.Event.Emit( GameLib.Event.SAVE_COMPONENT_ERROR, { message: response.message || 'The server responded but failed to save the component' } ) } } }; xhr.send(JSON.stringify({ component : data, session : this.token })); }; GameLib.System.Storage.prototype.loadComponent = function(toProcess, includeDependencies) { var loaded = []; var loading = []; toProcess.map(function(id) { if (loading.indexOf(id) === -1) { loading.push(id); } }); return function download(id, parentEntity) { var onComponentLoaded = this.onComponentLoaded; var onComponentProgress = this.onComponentProgress; var onComponentError = this.onComponentError; var xhr = new XMLHttpRequest(); xhr.onload = function(__system) { return function () { try { var object = JSON.parse(this.responseText); } catch (error) { if (onComponentError) { onComponentError(error); } GameLib.Event.Emit( GameLib.Event.LOAD_COMPONENT_ERROR, { error: error } ); return; } if (object.result !== 'success') { if (onComponentError) { onComponentError(id, object); } GameLib.Event.Emit( GameLib.Event.LOAD_COMPONENT_ERROR, {error : object} ); return; } /** * Now we need to create the runtime component - this happens systematically. * First, we create an API object from the Object, then a Runtime object from the API object * Each component has a function 'FromObject' which essentially does this for you */ object.component.map(function (component) { var componentName = GameLib.Component.GetComponentName(component.componentType); var componentClass = eval(componentName); var fn = componentClass['FromObject']; var runtimeComponent = null; if (component.componentType === GameLib.Component.COMPONENT_ENTITY) { runtimeComponent = fn(component, GameLib.EntityManager.Instance); runtimeComponent.parentEntity = parentEntity; parentEntity = runtimeComponent; } else { runtimeComponent = fn(__system.graphics, component); runtimeComponent.parentEntity = parentEntity; } if (runtimeComponent instanceof GameLib.D3.Image) { GameLib.Event.Emit( GameLib.Event.LOAD_IMAGE, { image : runtimeComponent } ) } loaded.push(runtimeComponent); if (includeDependencies) { /** * Before we announce the creation of this component, we should get * a list of all dependencies of this component, because once we announce * the creation of this component - the linking system will attempt to resolve * all dependencies */ var dependencies = runtimeComponent.getDependencies(); /** * Now - we should systematically check if we have the dependency already * loaded (in our runtime environment) - if we have - we just ignore loading this dependency (for now) * * We don't override runtime versions of the same component in the database because the user * could be working with it and it should be the latest version. */ dependencies = dependencies.reduce( function (result, dependency) { if (GameLib.EntityManager.Instance.findComponentById(dependency)) { /** * Don't add the dependency */ } else { result.push(dependency); } return result; }, [] ); /** * Also check if we did not already load this component quite recently */ dependencies = dependencies.reduce( function (result, dependency) { var found = loaded.reduce( function (result, component) { if (component.id === dependency) { result = true; } return result; }, false ); if (!found) { result.push(dependency); } return result; }, [] ); /** * We should now check our 'loading' list and add all dependencies which are not already in there */ dependencies.map( function (dependency) { if (loading.indexOf(dependency) === -1) { loading.push(dependency); } } ) } /** * Ok - now we have a super good idea of which components still need to load - * they live in the 'loading' list. * * At this point - the runtime components are created, but they are not ready * to be used. They may have dependencies to other components, which still need * to load, or may never be loaded. * * It is however safe, to announce, that we created the * runtime version of it, however it could still have some dependencies. * * The Linking system will then kick in and try to resolve all dependencies */ if (onComponentLoaded) { onComponentLoaded(runtimeComponent); } GameLib.Event.Emit( GameLib.Event.COMPONENT_CREATED, { component: runtimeComponent } ); var toProcess = GameLib.Utils.Difference(loaded.map(function(component){return component.id}), loading); if (toProcess.length === 0) { GameLib.Event.Emit( GameLib.Event.COMPONENT_DOWNLOAD_COMPLETE, { loaded: loaded } ) } else { download.bind(__system)(toProcess.pop(), parentEntity); } }); }; }(this); xhr.onprogress = function(__id) { return function (progressEvent) { var progress = 0; if (progressEvent.total !== 0) { progress = Number(progressEvent.loaded / progressEvent.total); progress *= 100; } if (onComponentProgress) { onComponentProgress(__id, progress) } }; }(id); xhr.onerror = function(__id) { return function (error) { console.warn('component load failed for component ID ' + __id); if (onComponentError) { onComponentError(__id, error) } }; }(id); xhr.open( 'GET', this.apiUrl + '/component/load/' + id ); xhr.send(); }.bind(this); }; /** * 'Loads' data from a url */ GameLib.System.Storage.prototype.load = function(data) { if (typeof XMLHttpRequest === 'undefined') { console.log('Implement server side load here'); return; } if (data.ids && data.ids.length > 0) { this.loadComponent(data.ids, data.includeDependencies)(data.ids[0], null); } else { console.log('No components selected'); } }; GameLib.System.Storage.prototype.loadImage = function(data) { console.log('loading image : ' + data.image.name); var onLoaded = this.onImageLoaded; var onProgress = this.onImageProgress; var onError = this.onImageError; var image = data.image; var url = this.apiUploadUrl + image.path + '?ts=' + Date.now(); var preflight = new XMLHttpRequest(); preflight.withCredentials = true; preflight.open( 'OPTIONS', url ); preflight.setRequestHeader('Content-Type', 'application/json'); preflight.onload = function() { var xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('GET', url); xhr.setRequestHeader('Content-Type', image.contentType); xhr.responseType = 'blob'; xhr.onload = function() { try { if (this.response.type !== 'application/json') { var url = window.URL.createObjectURL(this.response); } else { if (onError) { onError(image, {message:'Image not found'}); return; } } } catch (error) { if (onError) { onError(image, {message:'Image not found'}); return; } } var img = document.createElement('img'); img.onload = function() { window.URL.revokeObjectURL(url); image.instance = img; image.loaded = true; image.publish( GameLib.Event.IMAGE_INSTANCE_CREATED, { image: image } ); if (onLoaded) { onLoaded(image); } }; img.src = url; }; xhr.onprogress = function(progressEvent) { var progress = 0; if (progressEvent.total !== 0) { progress = Number(progressEvent.loaded / progressEvent.total); progress *= 100; } if (onProgress) { onProgress(image, progress); } image.size = progressEvent.total; }; xhr.onerror = function(error) { console.warn('image load failed for image ' + image.name); if (onError) { onError(image, error) } }; xhr.send(); }; preflight.onerror = function(error) { console.warn('image pre-flight request failed for image ' + image.name); if (onError) { onError(image, error); } }; preflight.send(); }; GameLib.System.Storage.prototype.stop = function() { this.loginSubscription.remove(); this.loadSubscription.remove(); this.saveSubscription.remove(); this.loadImageSubscription.remove(); };