r3-legacy/src/game-lib-system-storage.js

575 lines
17 KiB
JavaScript

/**
* Storage System takes care loading and linking components and dependencies
* @param graphics
* @param physics
* @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,
physics,
apiSystem,
apiUrl,
token,
apiUploadUrl,
onImageLoaded,
onImageProgress,
onImageError,
onComponentLoaded,
onComponentProgress,
onComponentError
) {
this.graphics = graphics;
this.graphics.isNotThreeThrow();
this.physics = physics;
this.physics.isNotCannonThrow();
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 {
try {
runtimeComponent = fn(__system.graphics, component);
} catch (error) {
runtimeComponent = null;
}
if (!runtimeComponent) {
try {
runtimeComponent = fn(__system.physics, component);
} catch (error) {
runtimeComponent = null;
}
}
if (!runtimeComponent) {
throw new Error('Could not create a runtime component: ', 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();
};