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

557 lines
16 KiB
JavaScript
Raw Normal View History

2017-06-13 14:09:18 +02:00
/**
* Storage System takes care loading and linking components and dependencies
2017-06-28 17:09:06 +02:00
* @param graphics
2017-06-13 14:09:18 +02:00
* @param apiSystem GameLib.API.System
* @param apiUrl
* @param token
* @param apiUploadUrl
* @param onImageLoaded
* @param onImageProgress
* @param onImageError
2017-06-27 13:27:27 +02:00
* @param onComponentLoaded
* @param onComponentProgress
* @param onComponentError
2017-06-13 14:09:18 +02:00
* @constructor
*/
GameLib.System.Storage = function(
2017-06-28 17:09:06 +02:00
graphics,
2017-06-13 14:09:18 +02:00
apiSystem,
apiUrl,
token,
apiUploadUrl,
onImageLoaded,
onImageProgress,
2017-06-27 13:27:27 +02:00
onImageError,
onComponentLoaded,
onComponentProgress,
onComponentError
2017-06-13 14:09:18 +02:00
) {
2017-06-28 17:09:06 +02:00
this.graphics = graphics;
this.graphics.isNotThreeThrow();
GameLib.System.call(
this,
apiSystem
);
2017-06-13 14:09:18 +02:00
if (GameLib.Utils.UndefinedOrNull(apiUrl)) {
console.warn('Need an API URL for a storage system');
apiUrl = '';
2017-06-13 14:09:18 +02:00
}
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;
2017-06-27 13:27:27 +02:00
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;
2017-06-19 15:54:02 +02:00
this.loginSubscription = null;
this.saveSubscription = null;
this.loadSubscription = null;
this.loadImageSubscription = null;
2017-06-13 14:09:18 +02:00
};
GameLib.System.Storage.prototype = Object.create(GameLib.System.prototype);
GameLib.System.Storage.prototype.constructor = GameLib.System.Storage;
2017-06-19 15:54:02 +02:00
GameLib.System.Storage.prototype.start = function() {
this.loginSubscription = this.subscribe(
GameLib.Event.LOGGED_IN,
function(data) {
this.token = data.token;
}
);
this.saveSubscription = this.subscribe(
2017-06-19 21:35:51 +02:00
GameLib.Event.SAVE_COMPONENT,
2017-06-19 15:54:02 +02:00
this.save
);
this.loadSubscription = this.subscribe(
2017-06-19 21:35:51 +02:00
GameLib.Event.LOAD_COMPONENT,
2017-06-19 15:54:02 +02:00
this.load
);
this.loadImageSubscription = this.subscribe(
GameLib.Event.LOAD_IMAGE,
this.loadImage
);
2017-06-19 15:54:02 +02:00
};
2017-06-13 14:09:18 +02:00
/**
* '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(
2017-06-19 21:35:51 +02:00
GameLib.Event.SAVE_COMPONENT_ERROR,
2017-06-13 14:09:18 +02:00
{
message: this.responseText
}
)
}
if (response.result === 'success') {
GameLib.Event.Emit(
2017-06-19 21:35:51 +02:00
GameLib.Event.COMPONENT_SAVED,
2017-06-13 14:09:18 +02:00
{
message: response.message || 'Successfully saved the component'
}
)
} else {
GameLib.Event.Emit(
2017-06-19 21:35:51 +02:00
GameLib.Event.SAVE_COMPONENT_ERROR,
2017-06-13 14:09:18 +02:00
{
message: response.message || 'The server responded but failed to save the component'
}
)
}
}
};
xhr.send(JSON.stringify({
component : data,
session : this.token
}));
};
2017-06-28 17:09:06 +02:00
GameLib.System.Storage.prototype.loadComponent = function(toProcess, includeDependencies) {
2017-06-13 14:09:18 +02:00
2017-06-27 13:27:27 +02:00
var loaded = [];
2017-06-28 17:09:06 +02:00
var loading = [];
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
toProcess.map(function(id) {
if (loading.indexOf(id) === -1) {
loading.push(id);
}
});
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
return function download(id) {
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var onComponentLoaded = this.onComponentLoaded;
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var onComponentProgress = this.onComponentProgress;
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var onComponentError = this.onComponentError;
2017-06-27 13:27:27 +02:00
var xhr = new XMLHttpRequest();
2017-06-28 17:09:06 +02:00
xhr.onload = function(__system) {
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
return function () {
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
try {
var object = JSON.parse(this.responseText);
} catch (error) {
2017-06-19 21:35:51 +02:00
2017-06-28 17:09:06 +02:00
if (onComponentError) {
onComponentError(error);
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
GameLib.Event.Emit(
GameLib.Event.LOAD_COMPONENT_ERROR,
{
error: error
}
);
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
return;
2017-06-19 21:35:51 +02:00
}
2017-06-28 17:09:06 +02:00
if (object.result !== 'success') {
if (onComponentError) {
onComponentError(error);
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
GameLib.Event.Emit(
GameLib.Event.LOAD_COMPONENT_ERROR,
{
error: error
}
);
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
return;
}
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
/**
* 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) {
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var componentName = GameLib.Component.GetComponentName(component.componentType);
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var componentClass = eval(componentName);
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var fn = componentClass['FromObject'];
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
var runtimeComponent = null;
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
if (component.componentType === GameLib.Component.COMPONENT_ENTITY) {
runtimeComponent = fn(component, GameLib.EntityManager.Instance);
} else {
runtimeComponent = fn(__system.graphics, component);
}
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
if (runtimeComponent instanceof GameLib.D3.Image) {
GameLib.Event.Emit(
GameLib.Event.LOAD_IMAGE,
{
image : runtimeComponent
}
)
}
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
loaded.push(runtimeComponent.id);
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)
* 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);
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
2017-06-27 13:27:27 +02:00
return result;
},
2017-06-28 17:09:06 +02:00
[]
2017-06-27 13:27:27 +02:00
);
2017-06-28 17:09:06 +02:00
/**
* 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);
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
2017-06-27 13:27:27 +02:00
return result;
},
2017-06-28 17:09:06 +02:00
[]
2017-06-27 13:27:27 +02:00
);
2017-06-28 17:09:06 +02:00
/**
* 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);
}
}
)
}
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
/**
* 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);
}
2017-06-27 13:27:27 +02:00
2017-06-28 17:09:06 +02:00
GameLib.Event.Emit(
GameLib.Event.COMPONENT_CREATED,
{
component: runtimeComponent
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
);
var toProcess = GameLib.Utils.Difference(loaded, loading);
if (toProcess.length === 0) {
GameLib.Event.Emit(
GameLib.Event.COMPONENT_DOWNLOAD_COMPLETE,
{
loaded: loaded
}
)
} else {
download.bind(__system)(toProcess.pop());
2017-06-13 14:09:18 +02:00
}
2017-06-28 17:09:06 +02:00
});
};
}(this);
2017-06-27 13:27:27 +02:00
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();
2017-06-28 17:09:06 +02:00
}.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;
2017-06-27 13:27:27 +02:00
}
2017-06-28 17:09:06 +02:00
if (data.ids && data.ids.length > 0) {
this.loadComponent(data.ids, data.includeDependencies)(data.ids[0]);
} else {
console.log('No components selected');
}
2017-06-27 13:27:27 +02:00
2017-06-19 15:54:02 +02:00
};
GameLib.System.Storage.prototype.loadImage = function(data) {
console.log('loading image : ' + data.image.name);
var onLoaded = this.onImageLoaded;
2017-06-27 13:27:27 +02:00
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;
2017-06-28 17:09:06 +02:00
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();
};
2017-06-19 15:54:02 +02:00
GameLib.System.Storage.prototype.stop = function() {
this.loginSubscription.remove();
this.loadSubscription.remove();
this.saveSubscription.remove();
this.loadImageSubscription.remove();
2017-06-19 15:54:02 +02:00
};