diff --git a/source/main.cpp b/source/main.cpp index 6c556aec77..2a6a71818d 100755 --- a/source/main.cpp +++ b/source/main.cpp @@ -2,6 +2,10 @@ #include #include +// Alan: For some reason if this gets included after anything else some +// compile time errors get thrown up todo with javascript internal typedefs +#include "scripting/ScriptingHost.h" + #include "sdl.h" #include "ogl.h" #include "detect.h" @@ -25,6 +29,7 @@ #include "EntityHandles.h" #include "EntityManager.h" + #ifndef NO_GUI #include "gui/GUI.h" #endif @@ -433,6 +438,8 @@ int main(int argc, char* argv[]) ParseArgs(argc, argv); + // Create the scripting host. This needs to be done before the GUI is created. + new ScriptingHost; // GUI is notified in set_vmode, so this must come before that. #ifndef NO_GUI @@ -597,6 +604,7 @@ if(!g_MapFile) delete CGUI::GetSingletonPtr(); // again, we should have all singleton deletes somewhere #endif + delete &g_ScriptingHost; delete &g_Config; delete &g_EntityManager; delete &g_EntityTemplateCollection; diff --git a/source/scripting/ScriptCustomTypes.cpp b/source/scripting/ScriptCustomTypes.cpp new file mode 100755 index 0000000000..00e061d785 --- /dev/null +++ b/source/scripting/ScriptCustomTypes.cpp @@ -0,0 +1,38 @@ + +#include "ScriptingHost.h" + +// POINT2D + +JSClass Point2dClass = +{ + "Point2d", 0, + JS_PropertyStub, JS_PropertyStub, + JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub, + JS_ConvertStub, JS_FinalizeStub +}; + +JSPropertySpec Point2dProperties[] = +{ + {"x", 0, JSPROP_ENUMERATE}, + {"y", 1, JSPROP_ENUMERATE}, + {0} +}; + +JSBool Point2d_Constructor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + if (argc == 2) + { + g_ScriptingHost.SetObjectProperty(obj, "x", argv[0]); + g_ScriptingHost.SetObjectProperty(obj, "y", argv[1]); + } + else + { + jsval zero = INT_TO_JSVAL(0); + g_ScriptingHost.SetObjectProperty(obj, "x", zero); + g_ScriptingHost.SetObjectProperty(obj, "y", zero); + } + + return JS_TRUE; +} + diff --git a/source/scripting/ScriptCustomTypes.h b/source/scripting/ScriptCustomTypes.h new file mode 100755 index 0000000000..c3fa5dee3d --- /dev/null +++ b/source/scripting/ScriptCustomTypes.h @@ -0,0 +1,17 @@ + +#ifndef _SCRIPTCUSTOMTYPES_H_ +#define _SCRIPTCUSTOMTYPES_H_ + +// Custom object types + +// Whilst Point2d is fully coded, it is never registered so is not available in script +// This is mostly as a demonstration of what you need to code to add a new type + +// VECTOR2D +extern JSClass Point2dClass; +extern JSPropertySpec Point2dProperties[]; +JSBool Point2d_Constructor(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval); + + + +#endif diff --git a/source/scripting/ScriptGlue.cpp b/source/scripting/ScriptGlue.cpp new file mode 100755 index 0000000000..48dd669b7e --- /dev/null +++ b/source/scripting/ScriptGlue.cpp @@ -0,0 +1,48 @@ + +#include "ScriptGlue.h" + +// Parameters for the table are: + +// 1: The name the function will be called as from script +// 2: The number of aguments this function expects +// 3: Depreciated, always zero +// 4: Reserved for future use, always zero + +JSFunctionSpec ScriptFunctionTable[] = +{ + {"WriteLog", WriteLog, 1, 0, 0}, + + {0, 0, 0, 0, 0}, +}; + +// Allow scripts to output to the global log file +JSBool WriteLog(JSContext * context, JSObject * globalObject, unsigned int argc, jsval * argv, jsval * rval) +{ + if (argc < 1) + return JS_FALSE; + + for (int i = 0; i < (int)argc; i++) + { + if (JSVAL_IS_INT(argv[i])) + { + printf("%d", JSVAL_TO_INT(argv[i])); + } + + if (JSVAL_IS_DOUBLE(argv[i])) + { + double d = g_ScriptingHost.ValueToDouble(argv[i]); + printf("%e", d); + } + + if (JSVAL_IS_STRING(argv[i])) + { + JSString * str = JS_ValueToString(context, argv[i]); + char * chars = JS_GetStringBytes(str); + printf(chars); + } + } + + printf("\n"); + + return JS_TRUE; +} diff --git a/source/scripting/ScriptGlue.h b/source/scripting/ScriptGlue.h new file mode 100755 index 0000000000..e4586e5de7 --- /dev/null +++ b/source/scripting/ScriptGlue.h @@ -0,0 +1,11 @@ + +#ifndef _SCRIPTGLUE_H_ +#define _SCRIPTGLUE_H_ + +#include "ScriptingHost.h" + +JSBool WriteLog(JSContext * context, JSObject * globalObject, unsigned int argc, jsval *argv, jsval *rval); + +JSFunctionSpec ScriptFunctionTable[]; + +#endif diff --git a/source/scripting/ScriptingHost.cpp b/source/scripting/ScriptingHost.cpp new file mode 100755 index 0000000000..6805af87e9 --- /dev/null +++ b/source/scripting/ScriptingHost.cpp @@ -0,0 +1,319 @@ + +#include "ScriptingHost.h" +#include "ScriptGlue.h" +#include +#include +#include + +#pragma comment (lib, "js32.lib") + +namespace +{ + const int RUNTIME_MEMORY_ALLOWANCE = 16 * 1024 * 1024; + const int STACK_CHUNK_SIZE = 16 * 1024; + + JSClass GlobalClass = + { + "global", 0, + JS_PropertyStub, JS_PropertyStub, + JS_PropertyStub, JS_PropertyStub, + JS_EnumerateStub, JS_ResolveStub, + JS_ConvertStub, JS_FinalizeStub + }; +} + +ScriptingHost::ScriptingHost() : m_RunTime(NULL), m_Context(NULL), m_GlobalObject(NULL) +{ + m_RunTime = JS_NewRuntime(RUNTIME_MEMORY_ALLOWANCE); + + if (m_RunTime == NULL) + { + throw (std::string("ScriptingHost: Failed to create JavaScript runtime")); + } + + m_Context = JS_NewContext(m_RunTime, STACK_CHUNK_SIZE); + + if (m_Context == NULL) + { + throw (std::string("ScriptingHost: Failed to create JavaScript context")); + } + + JS_SetErrorReporter(m_Context, ScriptingHost::ErrorReporter); + + m_GlobalObject = JS_NewObject(m_Context, &GlobalClass, NULL, NULL); + + if (m_GlobalObject == NULL) + { + throw (std::string("ScriptingHost: Failed to create global object")); + } + + if (JS_InitStandardClasses(m_Context, m_GlobalObject) == JSVAL_FALSE) + { + throw (std::string("ScriptingHost: Failed to init standard classes")); + } + + if (JS_DefineFunctions(m_Context, m_GlobalObject, ScriptFunctionTable) == JS_FALSE) + { + throw (std::string("ScriptingHost: Failed to setup native functions")); + } + + std::cout << "Scripting environment initialized" << std::endl; +} + +ScriptingHost::~ScriptingHost() +{ + if (m_Context != NULL) + { + JS_DestroyContext(m_Context); + m_Context = NULL; + } + + if (m_RunTime != NULL) + { + JS_DestroyRuntime(m_RunTime); + m_RunTime = NULL; + } +} + +void ScriptingHost::LoadScriptFromDisk(const std::string & fileName) +{ + std::string script; + std::string line; + + std::ifstream scriptFile(fileName.c_str()); + + if (scriptFile.is_open() == false) + { + throw (std::string("Could not open file: ") + fileName); + } + + while (scriptFile.eof() == false) + { + std::getline(scriptFile, line); + script += line; + script += '\n'; + } + + jsval rval; + JSBool ok = JS_EvaluateScript(m_Context, m_GlobalObject, script.c_str(), (unsigned int)script.length(), fileName.c_str(), 0, &rval); + + if (ok == JS_FALSE) + { + throw (std::string("Error loading script from disk: ") + fileName); + } +} + +jsval ScriptingHost::CallFunction(const std::string & functionName, jsval * params, int numParams) +{ + jsval result; + + JSBool ok = JS_CallFunctionName(m_Context, m_GlobalObject, functionName.c_str(), numParams, params, &result); + + if (ok == JS_FALSE) + { + throw (std::string("Failure whilst calling script funtion: ") + functionName); + } + + return result; +} + +jsval ScriptingHost::ExecuteScript(const std::string & script) +{ + jsval rval; + + JSBool ok = JS_EvaluateScript(m_Context, m_GlobalObject, script.c_str(), (int)script.length(), "Console", 0, &rval); + + return rval; +} + +void ScriptingHost::RegisterFunction(const std::string & functionName, JSNative function, int numArgs) +{ + JSFunction * func = JS_DefineFunction(m_Context, m_GlobalObject, functionName.c_str(), function, numArgs, 0); + + if (func == NULL) + { + throw (std::string("Could not register function ") + functionName); + } +} + +void ScriptingHost::DefineConstant(const std::string & name, int value) +{ + // First remove this constant if it already exists + JS_DeleteProperty(m_Context, m_GlobalObject, name.c_str()); + + JSBool ok = JS_DefineProperty( m_Context, m_GlobalObject, name.c_str(), INT_TO_JSVAL(value), + NULL, NULL, JSPROP_READONLY); + + if (ok == JS_FALSE) + { + throw (std::string("Could not create constant")); + } +} + +void ScriptingHost::DefineConstant(const std::string & name, double value) +{ + // First remove this constant if it already exists + JS_DeleteProperty(m_Context, m_GlobalObject, name.c_str()); + + struct JSConstDoubleSpec spec[2]; + + spec[0].name = name.c_str(); + spec[0].dval = value; + spec[0].flags = JSPROP_READONLY; + + spec[1].name = 0; + spec[1].dval = 0.0; + spec[1].flags = 0; + + JSBool ok = JS_DefineConstDoubles(m_Context, m_GlobalObject, spec); + + if (ok == JS_FALSE) + { + throw (std::string("Could not create constant")); + } +} + +void ScriptingHost::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uintN minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) +{ + std::string typeName = clasp->name; + + if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) + { + // This type already exists + throw std::string("Type already exists"); + } + + JSObject * obj = JS_InitClass( m_Context, m_GlobalObject, 0, + clasp, + constructor, minArgs, // Constructor, min args + ps, fs, // Properties, methods + static_ps, static_fs); // Constructor properties, methods + + if (obj != NULL) + { + CustomType type; + + type.m_Object = obj; + type.m_Class = clasp; + + m_CustomObjectTypes[typeName] = type; + } + else + { + throw std::string("Type creation failed"); + } +} + +JSObject * ScriptingHost::CreateCustomObject(const std::string & typeName) +{ + std::map < std::string, CustomType > ::iterator it = m_CustomObjectTypes.find(typeName); + + if (it == m_CustomObjectTypes.end()) + { + throw std::string("Tried to create a type that doesn't exist"); + } + + return JS_NewObject(m_Context, (*it).second.m_Class, (*it).second.m_Object, NULL); + +} + +void ScriptingHost::SetObjectProperty(JSObject * object, const std::string & propertyName, jsval value) +{ + JS_SetProperty(m_Context, object, propertyName.c_str(), &value); +} + +int ScriptingHost::ValueToInt(const jsval value) +{ + int i = 0; + + JSBool ok = JS_ValueToInt32(m_Context, value, (int32*)&i); + + if (ok == JS_FALSE) + { + throw (std::string("Convert to int failed")); + } + + return i; +} + +bool ScriptingHost::ValueToBool(const jsval value) +{ + JSBool b; + + JSBool ok = JS_ValueToBoolean(m_Context, value, &b); + + if (ok == JS_FALSE) + { + throw (std::string("Convert to bool failed")); + } + + return b == JS_TRUE; +} + +std::string ScriptingHost::ValueToString(const jsval value) +{ + JSString * string = JS_ValueToString(m_Context, value); + + return std::string(JS_GetStringBytes(string)); +} + +double ScriptingHost::ValueToDouble(const jsval value) +{ + jsdouble d; + + JSBool ok = JS_ValueToNumber(m_Context, value, &d); + + if (ok == JS_FALSE) + { + throw (std::string("Convert to double failed")); + } + + return d; +} + +void ScriptingHost::ErrorReporter(JSContext * context, const char * message, JSErrorReport * report) +{ + if (report->filename != NULL) + { + std::cout << report->filename << " (" << report->lineno << ") "; + } + + if (message != NULL) + { + std::cout << message << std::endl; + } + else + { + std::cout << "No error message available" << std::endl; + } +} + +void ScriptingHost::Tick(float timeElapsed) +{ + if (timeElapsed > 0.2f) timeElapsed = 0.2f; + + for (int i = 0; i < (int)m_DelayedScripts.size(); ) + { + m_DelayedScripts[i].m_SecondsToExecution -= timeElapsed; + + if (m_DelayedScripts[i].m_SecondsToExecution <= 0.0f) + { + this->ExecuteScript(m_DelayedScripts[i].m_FunctionName); + m_DelayedScripts.erase(m_DelayedScripts.begin() + i); + } + else + { + i++; + } + } +} + +void ScriptingHost::AddDelayedScript(const std::string & functionName, float delaySeconds) +{ + m_DelayedScripts.push_back(DelayedScriptExecutor(functionName, delaySeconds)); +} + +void ScriptingHost::_CollectGarbage() +{ + JS_GC(m_Context); +} diff --git a/source/scripting/ScriptingHost.h b/source/scripting/ScriptingHost.h new file mode 100755 index 0000000000..4c82c8a995 --- /dev/null +++ b/source/scripting/ScriptingHost.h @@ -0,0 +1,83 @@ + +#ifndef _SCRIPTINGHOST_H_ +#define _SCRIPTINGHOST_H_ + +#define XP_WIN +#include + +#include +#include +#include + +#include "Singleton.h" + +class DelayedScriptExecutor +{ +public: + DelayedScriptExecutor(const std::string & functionName, float delaySeconds) + : m_FunctionName(functionName), m_SecondsToExecution(delaySeconds) + { + } + + std::string m_FunctionName; + float m_SecondsToExecution; +}; + +class CustomType +{ +public: + JSObject * m_Object; + JSClass * m_Class; +}; + +class ScriptingHost : public Singleton < ScriptingHost > +{ +private: + JSRuntime * m_RunTime; + JSContext * m_Context; + JSObject * m_GlobalObject; + + JSErrorReport m_ErrorReport; + + std::vector < DelayedScriptExecutor > m_DelayedScripts; + std::map < std::string, CustomType > m_CustomObjectTypes; + + void _CollectGarbage(); + +public: + + ScriptingHost(); + ~ScriptingHost(); + + void LoadScriptFromDisk(const std::string & fileName); + + jsval CallFunction(const std::string & functionName, jsval * params, int numParams); + + jsval ExecuteScript(const std::string & script); + + void RegisterFunction(const std::string & functionName, JSNative function, int numArgs); + + void DefineConstant(const std::string & name, int value); + void DefineConstant(const std::string & name, double value); + + void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uintN nargs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); + + JSObject * CreateCustomObject(const std::string & typeName); + + void SetObjectProperty(JSObject * object, const std::string & propertyName, jsval value); + + int ValueToInt(const jsval value); + bool ValueToBool(const jsval value); + std::string ValueToString(const jsval value); + double ValueToDouble(const jsval value); + + void Tick(float timeElapsed); + + void AddDelayedScript(const std::string & functionName, float delaySeconds); + + static void ErrorReporter(JSContext * context, const char * message, JSErrorReport * report); +}; + +#define g_ScriptingHost ScriptingHost::GetSingleton() + +#endif