/** * Storage System takes care loading and linking components and dependencies * @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( apiSystem, apiUrl, token, apiUploadUrl, onImageLoaded, onImageProgress, onImageError, onComponentLoaded, onComponentProgress, onComponentError ) { 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 })); }; /** * 'Loads' data from a url */ GameLib.System.Storage.prototype.load = function(data) { if (typeof XMLHttpRequest === 'undefined') { console.log('Implement server side load here'); return; } /** * Load all the ids into our 'loading' list */ var loading = data.ids.reduce( function(result, id) { if (result.indexOf(id) === -1) { result.push(id); } return result; }, false ); var loaded = []; var includeDependencies = data.includeDependencies; var onComponentLoaded = this.onComponentLoaded; var onComponentProgress = this.onComponentProgress; var onComponentError = this.onComponentError; while (loading.length > 0) { var id = loading.pop(); var xhr = new XMLHttpRequest(); xhr.onload = 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(error); } GameLib.Event.Emit( GameLib.Event.LOAD_COMPONENT_ERROR, { error: error } ); 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 */ var componentName = GameLib.Component.GetComponentName(object.componentType); var componentClass = eval(componentName); var fn = componentClass['FromObject']; var runtimeComponent = null; if (object.componentType === GameLib.Component.COMPONENT_ENTITY) { runtimeComponent = fn(object, GameLib.EntityManager.Instance); } else { runtimeComponent = fn(this.graphics, object); } loaded.push(runtimeComponent.id); if (includeDependencies) { /** * Before we announce the creation of this component, we should get * a list of all dependencies of this object, because once we announce * the creation of this object - the linking system will attempt to resolve * all dependencies */ var dependencies = runtimeComponent.dependencies.map(function (dependency) { return dependency; }); /** * 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) * TODO: decide what to do with runtime versions of 'stale' dependencies */ var components = GameLib.EntityManager.Instance.queryComponents(componentClass); dependencies = dependencies.reduce( function (result, dependency) { var found = components.reduce( function (result, component) { if (component.id === dependency) { found = true; } return result; }, false ); if (!found) { 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, id) { if (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 objects, 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 } ) }; 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(); } }; 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.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(); };