/** Script is the component in charge of executing scripts to control the behaviour of the application.
* Script must be coded in Javascript and they have full access to all the engine, so one script could replace the behaviour of any part of the engine.
* Scripts are executed inside their own context, the context is local to the script so any variable defined in the context that is not attached to the context wont be accessible from other parts of the engine.
* To interact with the engine Scripts must bind callback to events so the callbacks will be called when those events are triggered, however, there are some generic methods that will be called
* @class Script
* @constructor
* @param {Object} object to configure from
*/
function Script(o)
{
this.enabled = true;
this.code = this.constructor.templates["component"];
this._script = new LScript();
this._script.extra_methods = {
getComponent: (function() { return this; }).bind(this),
getLocator: function() { return this.getComponent().getLocator() + "/context"; },
createProperty: LS.Component.prototype.createProperty,
createAction: LS.Component.prototype.createAction,
bind: LS.Component.prototype.bind,
unbind: LS.Component.prototype.unbind,
unbindAll: LS.Component.prototype.unbindAll
};
this._script.onerror = this.onError.bind(this);
this._script.exported_functions = [];
this._last_error = null;
this._breakpoint_on_call = false;
if(o)
this.configure(o);
}
Script.secure_module = false; //this module is not secure (it can execute code)
Script.block_execution = false; //avoid executing code
Script.catch_important_exceptions = true; //catch exception during parsing, otherwise configuration could fail
Script.icon = "mini-icon-script.png";
Script.templates = {
"component":"//@unnamed\n//defined: component, node, scene, globals\nthis.onStart = function()\n{\n}\n\nthis.onUpdate = function(dt)\n{\n\t//node.scene.refresh();\n}"
};
Script["@code"] = {type:'script'};
//used to determine to which object to bind
Script.BIND_TO_COMPONENT = 1;
Script.BIND_TO_NODE = 2;
Script.BIND_TO_SCENE = 3;
Script.BIND_TO_RENDERER = 4;
//Here we specify which methods of the script will be automatically binded to events in the system
//This way developing is much more easy as you dont have to bind or unbind anything
Script.API_functions = {};
Script.API_events_to_function = {};
Script.defineAPIFunction = function( func_name, target, event, info ) {
event = event || func_name;
target = target || Script.BIND_TO_SCENE;
var data = { name: func_name, target: target, event: event, info: info };
Script.API_functions[ func_name ] = data;
Script.API_events_to_function[ event ] = data;
}
//init
Script.defineAPIFunction( "onStart", Script.BIND_TO_SCENE, "start" );
Script.defineAPIFunction( "onFinish", Script.BIND_TO_SCENE, "finish" );
Script.defineAPIFunction( "onPrefabReady", Script.BIND_TO_NODE, "prefabReady" );
//behaviour
Script.defineAPIFunction( "onUpdate", Script.BIND_TO_SCENE, "update" );
Script.defineAPIFunction( "onNodeClicked", Script.BIND_TO_NODE, "node_clicked" );
Script.defineAPIFunction( "onClicked", Script.BIND_TO_NODE, "clicked" );
//rendering
Script.defineAPIFunction( "onSceneRender", Script.BIND_TO_SCENE, "beforeRender" );
Script.defineAPIFunction( "onCollectRenderInstances", Script.BIND_TO_NODE, "collectRenderInstances" ); //TODO: move to SCENE
Script.defineAPIFunction( "onRender", Script.BIND_TO_SCENE, "beforeRenderInstances" );
Script.defineAPIFunction( "onAfterRender", Script.BIND_TO_SCENE, "afterRenderInstances" );
Script.defineAPIFunction( "onRenderHelpers", Script.BIND_TO_SCENE, "renderHelpers" );
Script.defineAPIFunction( "onRenderGUI", Script.BIND_TO_SCENE, "renderGUI" );
Script.defineAPIFunction( "onEnableFrameContext", Script.BIND_TO_SCENE, "enableFrameContext" );
Script.defineAPIFunction( "onShowFrameContext", Script.BIND_TO_SCENE, "showFrameContext" );
//input
Script.defineAPIFunction( "onMouseDown", Script.BIND_TO_SCENE, "mousedown" );
Script.defineAPIFunction( "onMouseMove", Script.BIND_TO_SCENE, "mousemove" );
Script.defineAPIFunction( "onMouseUp", Script.BIND_TO_SCENE, "mouseup" );
Script.defineAPIFunction( "onMouseWheel", Script.BIND_TO_SCENE, "mousewheel" );
Script.defineAPIFunction( "onKeyDown", Script.BIND_TO_SCENE, "keydown" );
Script.defineAPIFunction( "onKeyUp", Script.BIND_TO_SCENE, "keyup" );
Script.defineAPIFunction( "onGamepadConnected", Script.BIND_TO_SCENE, "gamepadconnected" );
Script.defineAPIFunction( "onGamepadDisconnected", Script.BIND_TO_SCENE, "gamepaddisconnected" );
Script.defineAPIFunction( "onButtonDown", Script.BIND_TO_SCENE, "buttondown" );
Script.defineAPIFunction( "onButtonUp", Script.BIND_TO_SCENE, "buttonup" );
//dtor
Script.defineAPIFunction( "onDestroy", Script.BIND_TO_NODE, "destroy" );
Script.coding_help = "\n\
For a complete guide check: <a href='https://github.com/jagenjo/litescene.js/blob/master/guides/scripting.md' target='blank'>Scripting Guide</a>\n\
Global vars:\n\
+ node : represent the node where this component is attached.\n\
+ component : represent the component.\n\
+ this : represents the script context\n\
\n\
Some of the common API functions:\n\
+ onStart: when the Scene starts\n\
+ onUpdate: when updating\n\
+ onClicked : if this node is clicked (requires InteractiveController in root)\n\
+ onRender : before rendering the node\n\
+ onRenderGUI : to render something in the GUI using canvas2D\n\
+ onCollectRenderInstances: when collecting instances\n\
+ onAfterRender : after rendering the node\n\
+ onPrefabReady: when the prefab has been loaded\n\
+ onFinish : when the scene finished (mostly used for editor stuff)\n\
\n\
Remember, all basic vars attached to this will be exported as global.\n\
";
Script.active_scripts = {};
Object.defineProperty( Script.prototype, "context", {
set: function(v){
console.error("Script: context cannot be assigned");
},
get: function() {
if(this._script)
return this._script._context;
return null;
},
enumerable: false //if it was enumerable it would be serialized
});
Object.defineProperty( Script.prototype, "name", {
set: function(v){
console.error("Script: name cannot be assigned");
},
get: function() {
if(!this._script.code)
return;
var line = this._script.code.substr(0,32);
if(line.indexOf("//@") != 0)
return null;
var last = line.indexOf("\n");
if(last == -1)
last = undefined;
return line.substr(3,last - 3);
},
enumerable: false //if it was enumerable it would be serialized
});
Script.prototype.configure = function(o)
{
if(o.uid)
this.uid = o.uid;
if(o.enabled !== undefined)
this.enabled = o.enabled;
if(o.code !== undefined)
this.code = o.code;
if(this._root && this._root.scene)
this.processCode();
//do this before processing the code if you want the script to overwrite the vars
if(o.properties)
this.setContextProperties( o.properties );
}
Script.prototype.serialize = function()
{
return {
uid: this.uid,
enabled: this.enabled,
code: this.code,
properties: LS.cloneObject( this.getContextProperties() )
};
}
Script.prototype.getContext = function()
{
if(this._script)
return this._script._context;
return null;
}
Script.prototype.getCode = function()
{
return this.code;
}
Script.prototype.setCode = function( code, skip_events )
{
this.code = code;
this.processCode( skip_events );
}
/**
* This is the method in charge of compiling the code and executing the constructor, which also creates the context.
* It is called everytime the code is modified, that implies that the context is created when the component is configured.
* @method processCode
*/
Script.prototype.processCode = function( skip_events )
{
this._script.code = this.code;
if(!this._root || LS.Script.block_execution )
return true;
//unbind old stuff
if(this._script && this._script._context)
this._script._context.unbindAll();
//save old state
var old = this._stored_properties || this.getContextProperties();
//compiles and executes the context
var ret = this._script.compile({component:this, node: this._root, scene: this._root.scene, globals: LS.Globals });
if(!skip_events)
this.hookEvents();
this.setContextProperties( old );
this._stored_properties = null;
//execute some starter functions
if( this._script._context && !this._script._context._initialized )
{
if( this._root && this._script._context.onAddedToNode)
this._script._context.onAddedToNode( this._root );
if( this._root && this._root.scene )
{
if( this._script._context.onAddedToScene )
this._script._context.onAddedToScene( this._root.scene );
if( this._script._context.onBind )
this._script._context.onBind( this._root.scene );
if( this._root.scene._state === LS.RUNNING && this._script._context.start )
this._script._context.start();
}
this._script._context._initialized = true; //avoid initializing it twice
}
return ret;
}
Script.prototype.getContextProperties = function()
{
var ctx = this.getContext();
if(!ctx)
return;
return LS.cloneObject( ctx );
}
Script.prototype.setContextProperties = function( properties )
{
if(!properties)
return;
var ctx = this.getContext();
if(!ctx) //maybe the context hasnt been crated yet
{
this._stored_properties = properties;
return;
}
LS.cloneObject( properties, ctx, false, true );
}
//used for graphs
Script.prototype.setProperty = function(name, value)
{
var ctx = this.getContext();
if( ctx && ctx[name] !== undefined )
{
if(ctx[name].set)
ctx[name](value);
else
ctx[name] = value;
}
else if(this[name])
this[name] = value;
}
Script.prototype.getPropertiesInfo = function()
{
var ctx = this.getContext();
if(!ctx)
return {enabled:"boolean"};
var attrs = LS.getObjectProperties( ctx );
attrs.enabled = "boolean";
return attrs;
}
/*
Script.prototype.getPropertyValue = function( property )
{
var ctx = this.getContext();
if(!ctx)
return;
return ctx[ property ];
}
Script.prototype.setPropertyValue = function( property, value )
{
var context = this.getContext();
if(!context)
return;
if( context[ property ] === undefined )
return;
if(context[ property ] && context[ property ].set)
context[ property ].set( value );
else
context[ property ] = value;
return true;
}
*/
//used for animation tracks
Script.prototype.getPropertyInfoFromPath = function( path )
{
if(path[0] != "context")
return;
var context = this.getContext();
if(path.length == 1)
return {
name:"context",
node: this._root,
target: context,
type: "object"
};
var varname = path[1];
if(!context || context[ varname ] === undefined )
return;
var value = context[ varname ];
var extra_info = context[ "@" + varname ];
if(!extra_info)
extra_info = context.constructor[ "@" + varname ];
var type = "";
if(extra_info)
type = extra_info.type;
if(!type && value !== null && value !== undefined)
{
if(value.constructor === String)
type = "string";
else if(value.constructor === Boolean)
type = "boolean";
else if(value.length)
type = "vec" + value.length;
else if(value.constructor === Number)
type = "number";
else if(value.constructor === Function)
type = "function";
}
if(type == "function")
value = varname; //just to avoid doing assignments of functions
return {
node: this._root,
target: context,
name: varname,
value: value,
type: type
};
}
Script.prototype.setPropertyValueFromPath = function( path, value, offset )
{
offset = offset || 0;
if( path.length < (offset+1) )
return;
if(path[offset] != "context" )
return;
var context = this.getContext();
var varname = path[offset+1];
if(!context || context[ varname ] === undefined )
return;
if( context[ varname ] === undefined )
return;
//cannot assign functions this way
if( context[ varname ] && context[ varname ].constructor == Function )
return;
if(context[ varname ] && context[ varname ].set)
context[ varname ].set( value );
else
context[ varname ] = value;
}
/**
* This check if the context has API functions that should be called, if thats the case, it binds events automatically
* This way we dont have to bind manually all the methods.
* @method hookEvents
*/
Script.prototype.hookEvents = function()
{
var node = this._root;
if(!node)
throw("hooking events of a Script without a node");
var scene = node.scene || LS.GlobalScene; //hack
//script context
var context = this.getContext();
if(!context)
return;
//hook events
for(var i in LS.Script.API_functions)
{
var func_name = i;
var event_info = LS.Script.API_functions[ func_name ];
var target = null;
switch( event_info.target )
{
case Script.BIND_TO_COMPONENT: target = this; break;
case Script.BIND_TO_NODE: target = node; break;
case Script.BIND_TO_SCENE: target = scene; break;
case Script.BIND_TO_RENDERER: target = LS.Renderer; break;
}
if(!target)
throw("Script event without target?");
//check if this function exist
if( context[ func_name ] && context[ func_name ].constructor === Function )
{
if( !LEvent.isBind( target, event_info.event, this.onScriptEvent, this ) )
LEvent.bind( target, event_info.event, this.onScriptEvent, this );
}
else //if it doesnt ensure we are not binding the event
LEvent.unbind( target, event_info.event, this.onScriptEvent, this );
}
}
/**
* Called every time an event should be redirected to one function in the script context
* @method onScriptEvent
*/
Script.prototype.onScriptEvent = function(event_type, params)
{
if(!this.enabled)
return;
var event_info = LS.Script.API_events_to_function[ event_type ];
if(!event_info)
return; //????
if(this._breakpoint_on_call)
{
this._breakpoint_on_call = false;
{{debugger}} //stops the execution if the console is open
}
return this._script.callMethod( event_info.name, params );
}
Script.prototype.onAddedToNode = function( node )
{
if(!node.script)
node.script = this;
}
Script.prototype.onRemovedFromNode = function( node )
{
if(node.script == this)
delete node.script;
}
Script.prototype.onAddedToScene = function( scene )
{
if( this._name && !LS.Script.active_scripts[ this._name ] )
LS.Script.active_scripts[ this._name ] = this;
//avoid to parse it again
if(this._script && this._script._context && this._script._context._initialized )
{
if(this._script._context.onBind)
this._script._context.onBind();
return;
}
if( !this.constructor.catch_important_exceptions )
{
this.processCode();
return;
}
//catch
try
{
//careful, if the code saved had an error, do not block the flow of the configure or the rest will be lost
this.processCode();
}
catch (err)
{
console.error(err);
}
}
Script.prototype.onRemovedFromScene = function(scene)
{
if( this._name && LS.Script.active_scripts[ this._name ] )
delete LS.Script.active_scripts[ this._name ];
//ensures no binded events
LEvent.unbindAll( scene, this );
if( this._context )
{
LEvent.unbindAll( scene, this._context, this );
if(this._script._context.onUnbind )
this._script._context.onUnbind( scene );
if(this._script._context.onRemovedFromScene )
this._context.onRemovedFromScene( scene );
}
}
//used in editor
Script.prototype.getComponentTitle = function()
{
return this.name; //name is a getter that reads the name from the code comment
}
//TODO stuff ***************************************
/*
Script.prototype.onAddedToProject = function( project )
{
try
{
//just in case the script saved had an error, do not block the flow
this.processCode();
}
catch (err)
{
console.error(err);
}
}
Script.prototype.onRemovedFromProject = function( project )
{
//ensures no binded events
if(this._context)
LEvent.unbindAll( project, this._context, this );
//unbind evends
LEvent.unbindAll( project, this );
}
*/
//*******************************
Script.prototype.onError = function(e)
{
var scene = this._root.scene;
if(!scene)
return;
e.script = this;
e.node = this._root;
LEvent.trigger( this, "code_error",e);
LEvent.trigger( scene, "code_error",e);
LEvent.trigger( LS, "code_error",e);
//conditional this?
console.log("app finishing due to error in script");
scene.finish();
}
//called from the editor?
Script.prototype.onCodeChange = function(code)
{
this.processCode();
}
Script.prototype.getResources = function(res)
{
var ctx = this.getContext();
if(ctx && ctx.onGetResources )
ctx.onGetResources( res );
}
LS.registerComponent( Script );
LS.Script = Script;
//*****************
function ScriptFromFile(o)
{
this.enabled = true;
this._filename = "";
this._script = new LScript();
this._script.extra_methods = {
getComponent: (function() { return this; }).bind(this),
getLocator: function() { return this.getComponent().getLocator() + "/context"; },
createProperty: LS.Component.prototype.createProperty,
createAction: LS.Component.prototype.createAction,
bind: LS.Component.prototype.bind,
unbind: LS.Component.prototype.unbind,
unbindAll: LS.Component.prototype.unbindAll
};
this._script.onerror = this.onError.bind(this);
this._script.exported_functions = [];//this.constructor.exported_callbacks;
this._last_error = null;
if(o)
this.configure(o);
}
ScriptFromFile.coding_help = Script.coding_help;
Object.defineProperty( ScriptFromFile.prototype, "filename", {
set: function(v){
if(v) //to avoid double slashes
v = LS.ResourcesManager.cleanFullpath( v );
this._filename = v;
this.processCode();
},
get: function() {
return this._filename;
},
enumerable: true
});
Object.defineProperty( ScriptFromFile.prototype, "context", {
set: function(v){
console.error("ScriptFromFile: context cannot be assigned");
},
get: function() {
if(this._script)
return this._script._context;
return null;
},
enumerable: false //if it was enumerable it would be serialized
});
Object.defineProperty( ScriptFromFile.prototype, "name", {
set: function(v){
console.error("Script: name cannot be assigned");
},
get: function() {
if(!this._script.code)
return;
var line = this._script.code.substr(0,32);
if(line.indexOf("//@") != 0)
return null;
var last = line.indexOf("\n");
if(last == -1)
last = undefined;
return line.substr(3,last - 3);
},
enumerable: false //if it was enumerable it would be serialized
});
ScriptFromFile.prototype.onAddedToScene = function( scene )
{
//avoid to parse it again
if(this._script && this._script._context && this._script._context._initialized )
{
if( this._script._context.onBind )
this._script._context.onBind( scene );
if( this._script._context.onAddedToScene )
this._script._context.onAddedToScene( scene );
return;
}
if( !this.constructor.catch_important_exceptions )
{
this.processCode();
return;
}
//catch
try
{
//careful, if the code saved had an error, do not block the flow of the configure or the rest will be lost
this.processCode();
}
catch (err)
{
console.error(err);
}
}
ScriptFromFile.prototype.processCode = function( skip_events )
{
var that = this;
if(!this.filename)
return;
var script_resource = LS.ResourcesManager.getResource( this.filename );
if(!script_resource)
{
LS.ResourcesManager.load( this.filename, null, function( res, url ){
if( url != that.filename )
return;
that.processCode( skip_events );
});
return;
}
var code = script_resource.data;
if( code === undefined || this._script.code == code )
return;
if(!this._root || LS.Script.block_execution )
return true;
//assigned inside because otherwise if it gets modified before it is attached to the scene tree then it wont be compiled
this._script.code = code;
//unbind old stuff
if( this._script && this._script._context )
this._script._context.unbindAll();
//compiles and executes the context
var old = this._stored_properties || this.getContextProperties();
var ret = this._script.compile({component:this, node: this._root, scene: this._root.scene, globals: LS.Globals });
if(!skip_events)
this.hookEvents();
this.setContextProperties( old );
this._stored_properties = null;
//try to catch up with all the events missed while loading the script
if( this._script._context && !this._script._context._initialized )
{
if( this._root && this._script._context.onAddedToNode)
this._script._context.onAddedToNode( this._root );
if( this._root && this._root.scene )
{
if( this._script._context.onAddedToScene )
this._script._context.onAddedToScene( this._root.scene );
if( this._script._context.onBind )
this._script._context.onBind( this._root.scene );
if( this._root.scene._state === LS.RUNNING && this._script._context.start )
this._script._context.start();
}
this._script._context._initialized = true; //avoid initializing it twice
}
return ret;
}
ScriptFromFile.prototype.configure = function(o)
{
if(o.uid)
this.uid = o.uid;
if(o.enabled !== undefined)
this.enabled = o.enabled;
if(o.filename !== undefined)
this.filename = o.filename;
if(o.properties)
this.setContextProperties( o.properties );
if(this._root && this._root.scene)
this.processCode();
}
ScriptFromFile.prototype.serialize = function()
{
return {
uid: this.uid,
enabled: this.enabled,
filename: this.filename,
properties: LS.cloneObject( this.getContextProperties() )
};
}
ScriptFromFile.prototype.getResources = function(res)
{
if(this.filename)
res[this.filename] = LS.Resource;
//script resources
var ctx = this.getContext();
if(!ctx || !ctx.getResources )
return;
ctx.getResources( res );
}
ScriptFromFile.prototype.getCodeResource = function()
{
return LS.ResourcesManager.getResource( this.filename );
}
ScriptFromFile.prototype.getCode = function()
{
var script_resource = LS.ResourcesManager.getResource( this.filename );
if(!script_resource)
return "";
return script_resource.data;
}
ScriptFromFile.prototype.setCode = function( code, skip_events )
{
var script_resource = LS.ResourcesManager.getResource( this.filename );
if(!script_resource)
return "";
script_resource.data = code;
this.processCode( skip_events );
}
ScriptFromFile.updateComponents = function( script, skip_events )
{
if( !script || !script._root )
return;
var filename = script.filename;
var scene = script._root.scene || LS.GlobalScene;
var components = scene.findNodeComponents( LS.ScriptFromFile );
for(var i = 0; i < components.length; ++i)
{
var compo = components[i];
var filename = script.fullpath || script.filename;
if( compo.filename == filename )
compo.processCode(skip_events);
}
}
LS.extendClass( ScriptFromFile, Script );
LS.registerComponent( ScriptFromFile );
LS.ScriptFromFile = ScriptFromFile;