diff --git a/binaries/data/mods/public/simulation/ai/common-api/base.js b/binaries/data/mods/public/simulation/ai/common-api/base.js new file mode 100644 index 0000000000..128ce8a5c8 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/common-api/base.js @@ -0,0 +1,54 @@ +function BaseAI(settings) +{ + if (!settings) + return; + + // Make some properties non-enumerable, so they won't be serialised + Object.defineProperty(this, "_player", {value: settings.player, enumerable: false}); + Object.defineProperty(this, "_templates", {value: settings.templates, enumerable: false}); +} + +BaseAI.prototype.HandleMessage = function(state) +{ + if (!this._rawEntities) + this._rawEntities = state.entities; + else + this.ApplyEntitiesDelta(state); + +//print("### "+uneval(state)+"\n\n"); +//print("@@@ "+uneval(this._rawEntities)+"\n\n"); + + this.entities = new EntityCollection(this, this._rawEntities); + + this.OnUpdate(); + + // Clean up temporary properties, so they don't disturb the serializer + delete this.entities; +}; + +BaseAI.prototype.ApplyEntitiesDelta = function(state) +{ + for each (var evt in state.events) + { + if (evt.type == "Destroy") + { + delete this._rawEntities[evt.msg.entity]; + } + } + + for (var id in state.entities) + { + var changes = state.entities[id]; + for (var prop in changes) + this._rawEntities[id][prop] = changes[prop]; + } +}; + +BaseAI.prototype.OnUpdate = function(state) +{ +}; + +BaseAI.prototype.chat = function(message) +{ + Engine.PostCommand({"type": "chat", "message": message}); +}; diff --git a/binaries/data/mods/public/simulation/ai/common-api/entity.js b/binaries/data/mods/public/simulation/ai/common-api/entity.js new file mode 100644 index 0000000000..db745ac50d --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -0,0 +1,123 @@ +function Entity(baseAI, entity) +{ + this._ai = baseAI; + this._entity = entity; + this._template = baseAI._templates[entity.template]; +} + +Entity.prototype = { + get rank() { + if (!this._template.Identity) + return undefined; + return this._template.Identity.Rank; + }, + + get classes() { + if (!this._template.Identity || !this._template.Identity.Classes) + return undefined; + return this._template.Identity.Classes._string.split(/\s+/); + }, + + get civ() { + if (!this._template.Identity) + return undefined; + return this._template.Identity.Civ; + }, + + + get position() { return this._entity.position; }, + + + get hitpoints() { return this._entity.hitpoints; }, + get maxHitpoints() { return this._template.Health.Max; }, + get isHurt() { return this.hitpoints < this.maxHitpoints; }, + get needsHeal() { return this.isHurt && (this._template.Health.Healable == "true"); }, + get needsRepair() { return this.isHurt && (this._template.Health.Repairable == "true"); }, + + + // TODO: attack, armour + + + get buildableEntities() { + if (!this._template.Builder) + return undefined; + var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/); + return templates; // TODO: map to Entity? + }, + + get trainableEntities() { + if (!this._template.TrainingQueue) + return undefined; + var templates = this._template.TrainingQueue.Entities._string.replace(/\{civ\}/g, this.civ).split(/\s+/); + return templates; + }, + + + get trainingQueue() { return this._entity.trainingQueue; }, + + get foundationProgress() { return this._entities.foundationProgress; }, + + + get owner() { return this._entity.owner; }, + get isOwn() { return this._entity.owner == this._ai._player; }, + get isFriendly() { return this.isOwn; }, // TODO: diplomacy + get isEnemy() { return !this.isOwn; }, // TODO: diplomacy + + + get resourceSupplyType() { + if (!this._template.ResourceSupply) + return undefined; + var [type, subtype] = this._template.ResourceSupply.Type.split('.'); + return { "generic": type, "specific": subtype }; + }, + + get resourceSupplyMax() { + if (!this._template.ResourceSupply) + return undefined; + return +this._template.ResourceSupply.Amount; + }, + + get resourceSupplyAmount() { return this._entity.resourceSupplyAmount; }, + + + get resourceGatherRates() { + if (!this._template.ResourceGatherer) + return undefined; + var ret = {}; + for (var r in this._template.ResourceGatherer.Rates) + ret[r] = this._template.ResourceGatherer.Rates[r] * this._template.ResourceGatherer.BaseSpeed; + return ret; + }, + + get resourceCarrying() { return this._entity.resourceCarrying; }, + + + get resourceDropsiteTypes() { + if (!this._template.ResourceDropsite) + return undefined; + return this._template.ResourceDropsite.Types.split(/\s+/); + }, + + + get garrisoned() { return new EntityCollection(this._ai, this._entity.garrisoned); }, + + get garrisonableClasses() { + if (!this._template.GarrisonHolder) + return undefined; + return this._template.GarrisonHolder.List._string.split(/\s+/); + }, + + + // TODO: visibility + + + move: function(x, z) { + Engine.PostCommand({"type": "walk", "entities": [this.entity.id], "x": x, "z": z, "queued": false}); + return this; + }, + + destroy: function() { + Engine.PostCommand({"type": "delete-entities", "entities": [this.entity.id]}); + return this; + }, +}; diff --git a/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js new file mode 100644 index 0000000000..ed57ee6311 --- /dev/null +++ b/binaries/data/mods/public/simulation/ai/common-api/entitycollection.js @@ -0,0 +1,38 @@ +function EntityCollection(baseAI, entities) +{ + this._ai = baseAI; + this._entities = entities; +} + +EntityCollection.prototype.ToIdArray = function() +{ + var ret = []; + for (var id in this._entities) + ret.push(+id); + return ret; +}; + +EntityCollection.prototype.filter = function(callback, thisp) +{ + var ret = {}; + for (var id in this._entities) + { + var ent = this._entities[id]; + var val = new Entity(this._ai, ent); + if (callback.call(thisp, val, id, this)) + ret[id] = ent; + } + return new EntityCollection(this._ai, ret); +}; + +EntityCollection.prototype.move = function(x, z) +{ + Engine.PostCommand({"type": "walk", "entities": this.ToIdArray(), "x": x, "z": z, "queued": false}); + return this; +}; + +EntityCollection.prototype.destroy = function() +{ + Engine.PostCommand({"type": "delete-entities", "entities": this.ToIdArray()}); + return this; +}; diff --git a/binaries/data/mods/public/simulation/ai/scaredybot/scaredybot.js b/binaries/data/mods/public/simulation/ai/scaredybot/scaredybot.js index 553b89ba2a..671ae9304f 100644 --- a/binaries/data/mods/public/simulation/ai/scaredybot/scaredybot.js +++ b/binaries/data/mods/public/simulation/ai/scaredybot/scaredybot.js @@ -1,31 +1,27 @@ +Engine.IncludeModule("common-api"); + function ScaredyBotAI(settings) { warn("Constructing ScaredyBotAI for player "+settings.player); - this.player = settings.player; + BaseAI.call(this, settings); + this.turn = 0; this.suicideTurn = 20; } -ScaredyBotAI.prototype.HandleMessage = function(state) -{ -// print("### HandleMessage("+uneval(state)+")\n\n"); -// print(uneval(this)+"\n\n"); +ScaredyBotAI.prototype = new BaseAI(); +ScaredyBotAI.prototype.OnUpdate = function() +{ if (this.turn == 0) - { - Engine.PostCommand({"type": "chat", "message": "Good morning."}); - } + this.chat("Good morning."); if (this.turn == this.suicideTurn) { - Engine.PostCommand({"type": "chat", "message": "I quake in my boots! My troops cannot hope to survive against a power such as yours."}); + this.chat("I quake in my boots! My troops cannot hope to survive against a power such as yours."); - var myEntities = []; - for (var ent in state.entities) - if (state.entities[ent].player == this.player) - myEntities.push(+ent); - Engine.PostCommand({"type": "delete-entities", "entities": myEntities}); + this.entities.filter(function(ent) { return ent.isOwn; }).destroy(); } this.turn++; diff --git a/binaries/data/mods/public/simulation/components/AIInterface.js b/binaries/data/mods/public/simulation/components/AIInterface.js index 6e8ed3971f..8c4573af80 100644 --- a/binaries/data/mods/public/simulation/components/AIInterface.js +++ b/binaries/data/mods/public/simulation/components/AIInterface.js @@ -21,6 +21,15 @@ AIInterface.prototype.GetRepresentation = function() // Reset the event list for the next turn this.events = []; + // Add entity representations + state.entities = {}; + for each (var proxy in Engine.GetComponentsWithInterface(IID_AIProxy)) + { + var rep = proxy.GetRepresentation(); + if (rep !== null) + state.entities[proxy.entity] = rep; + } + return state; }; diff --git a/binaries/data/mods/public/simulation/components/AIProxy.js b/binaries/data/mods/public/simulation/components/AIProxy.js index dedee134f2..3ee14ce428 100644 --- a/binaries/data/mods/public/simulation/components/AIProxy.js +++ b/binaries/data/mods/public/simulation/components/AIProxy.js @@ -3,18 +3,161 @@ function AIProxy() {} AIProxy.prototype.Schema = ""; +/** + * AIProxy passes its entity's state data to AI scripts. + * + * Efficiency is critical: there can be many thousands of entities, + * and the data returned by this component is serialized and copied to + * the AI thread every turn, so it can be quite expensive. + * + * We omit all data that can be derived statically from the template XML + * files - the AI scripts can parse the templates themselves. + * This violates the component interface abstraction and is potentially + * fragile if the template formats change (since both the component code + * and the AI will have to be updated in sync), but it's not *that* bad + * really and it helps performance significantly. + * + * We also add an optimisation to avoid copying non-changing values. + * The first call to GetRepresentation calls GetFullRepresentation, + * which constructs the complete entity state representation. + * After that, we simply listen to events from the rest of the gameplay code, + * and store the changed data in this.changes. + * Properties in this.changes will override those previously returned + * from GetRepresentation; if a property isn't overridden then the AI scripts + * will keep its old value. + * + * The event handlers should set this.changes.whatever to exactly the + * same as GetFullRepresentation would set. + */ + AIProxy.prototype.Init = function() { + this.changes = null; + this.needsFullGet = true; }; AIProxy.prototype.GetRepresentation = function() { - // Currently we'll just return the same data that the GUI uses. - // Maybe we should add/remove things (or make it more efficient) - // later. + // Return the full representation the first time we're called + var ret; + if (this.needsFullGet) + { + ret = this.GetFullRepresentation(); + this.needsFullGet = false; + } + else + { + ret = this.changes; + } - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - return cmpGuiInterface.GetEntityState(-1, this.entity); + // Initialise changes to null instead of {}, to avoid memory allocations in the + // common case where there will be no changes; event handlers should each reset + // it to {} if needed + this.changes = null; + + return ret; +}; + +AIProxy.prototype.OnPositionChanged = function(msg) +{ + if (!this.changes) + this.changes = {}; + + if (msg.inWorld) + this.changes.position = [msg.x, msg.z]; + else + this.changes.position = undefined; +}; + +AIProxy.prototype.OnHealthChanged = function(msg) +{ + if (!this.changes) + this.changes = {}; + + this.changes.hitpoints = msg.to; +}; + +AIProxy.prototype.OnOwnershipChanged = function(msg) +{ + if (!this.changes) + this.changes = {}; + + this.changes.owner = msg.to; +}; + +// TODO: event handlers for all the other things + +AIProxy.prototype.GetFullRepresentation = function() +{ + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + + var ret = { + // These properties are constant and won't need to be updated + "id": this.entity, + "template": cmpTemplateManager.GetCurrentTemplateName(this.entity) + } + + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (cmpPosition) + { + // Updated by OnPositionChanged + + if (cmpPosition.IsInWorld()) + { + var pos = cmpPosition.GetPosition2D(); + ret.position = [pos.x, pos.y]; + } + else + { + ret.position = undefined; + } + } + + var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); + if (cmpHealth) + { + // Updated by OnHealthChanged + ret.hitpoints = cmpHealth.GetHitpoints(); + } + + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (cmpOwnership) + { + // Updated by OnOwnershipChanged + ret.owner = cmpOwnership.GetOwner(); + } + + var cmpTrainingQueue = Engine.QueryInterface(this.entity, IID_TrainingQueue); + if (cmpTrainingQueue) + { + ret.trainingQueue = cmpTrainingQueue.GetQueue(); + } + + var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); + if (cmpFoundation) + { + ret.foundationProgress = cmpFoundation.GetBuildPercentage(); + } + + var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); + if (cmpResourceSupply) + { + ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); + } + + var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (cmpResourceGatherer) + { + ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); + } + + var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (cmpGarrisonHolder) + { + ret.garrisoned = cmpGarrisonHolder.GetEntities(); + } + + return ret; }; Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy); diff --git a/binaries/data/mods/public/simulation/components/GarrisonHolder.js b/binaries/data/mods/public/simulation/components/GarrisonHolder.js index 322283dc74..db08aa7594 100644 --- a/binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ b/binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -22,7 +22,7 @@ GarrisonHolder.prototype.Schema = */ GarrisonHolder.prototype.Init = function() { - //Garrisoned Units + // Garrisoned Units this.entities = []; this.spaceOccupied = 0; this.timer = undefined; diff --git a/binaries/data/mods/public/simulation/components/interfaces/AIProxy.js b/binaries/data/mods/public/simulation/components/interfaces/AIProxy.js new file mode 100644 index 0000000000..9fec75f69f --- /dev/null +++ b/binaries/data/mods/public/simulation/components/interfaces/AIProxy.js @@ -0,0 +1 @@ +Engine.RegisterInterface("AIProxy"); diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index ac399cf71a..95b5ab3e00 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -179,7 +179,7 @@ void CNetServerWorker::Run() { // To avoid the need for JS_SetContextThread, we create and use and destroy // the script interface entirely within this network thread - m_ScriptInterface = new ScriptInterface("Engine", "Net server"); + m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime()); while (true) { diff --git a/source/network/tests/test_Net.h b/source/network/tests/test_Net.h index 9090bb8d0c..9555b5337b 100644 --- a/source/network/tests/test_Net.h +++ b/source/network/tests/test_Net.h @@ -136,7 +136,7 @@ public: // This doesn't actually test much, it just runs a very quick multiplayer game // and prints a load of debug output so you can see if anything funny's going on - ScriptInterface scriptInterface("Engine"); + ScriptInterface scriptInterface("Engine", "Test", ScriptInterface::CreateRuntime()); TestStdoutLogger logger; std::vector clients; diff --git a/source/network/tests/test_NetMessage.h b/source/network/tests/test_NetMessage.h index b21e3584a6..f25a2d84ef 100644 --- a/source/network/tests/test_NetMessage.h +++ b/source/network/tests/test_NetMessage.h @@ -26,7 +26,7 @@ class TestNetMessage : public CxxTest::TestSuite public: void test_sim() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CScriptValRooted val; script.Eval("[4]", val); CSimulationMessage msg(script, 1, 2, 3, val.get()); diff --git a/source/ps/Replay.cpp b/source/ps/Replay.cpp index 4dd39c4777..7d1b6fa43f 100644 --- a/source/ps/Replay.cpp +++ b/source/ps/Replay.cpp @@ -189,7 +189,7 @@ void CReplayPlayer::Replay() // std::string hash; // bool ok = game.GetSimulation2()->ComputeStateHash(hash); // debug_assert(ok); -// debug_printf(L"%hs\n", Hexify(hash).c_str()); +// debug_printf(L"%hs", Hexify(hash).c_str()); debug_printf(L"\n"); diff --git a/source/scripting/ScriptingHost.cpp b/source/scripting/ScriptingHost.cpp index c64668900d..01bf71d24b 100644 --- a/source/scripting/ScriptingHost.cpp +++ b/source/scripting/ScriptingHost.cpp @@ -29,7 +29,7 @@ ScriptingHost::ScriptingHost() { - m_ScriptInterface = new ScriptInterface("Engine", "GUI"); + m_ScriptInterface = new ScriptInterface("Engine", "GUI", ScriptInterface::CreateRuntime()); m_Context = m_ScriptInterface->GetContext(); diff --git a/source/scriptinterface/ScriptConversions.cpp b/source/scriptinterface/ScriptConversions.cpp index 8e291b7796..716497ba0c 100644 --- a/source/scriptinterface/ScriptConversions.cpp +++ b/source/scriptinterface/ScriptConversions.cpp @@ -261,3 +261,9 @@ VECTOR(u32) VECTOR(std::string) VECTOR(std::wstring) VECTOR(CScriptValRooted) + +class IComponent; +template<> jsval ScriptInterface::ToJSVal >(JSContext* cx, const std::vector& val) +{ + return ToJSVal_vector(cx, val); +} diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index aa209cfa3f..05ac61aa5e 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -48,18 +48,94 @@ const int STACK_CHUNK_SIZE = 8192; //////////////////////////////////////////////////////////////// +class ScriptRuntime +{ +public: + ScriptRuntime() : + m_rooter(NULL) + { + m_rt = JS_NewRuntime(RUNTIME_SIZE); + debug_assert(m_rt); // TODO: error handling + +#if ENABLE_SCRIPT_PROFILING + // Profiler isn't thread-safe, so only enable this on the main thread + if (ThreadUtil::IsMainThread()) + { + if (CProfileManager::IsInitialised()) + { + JS_SetExecuteHook(m_rt, jshook_script, this); + JS_SetCallHook(m_rt, jshook_function, this); + } + } +#endif + + JS_SetExtraGCRoots(m_rt, jshook_trace, this); + } + + ~ScriptRuntime() + { + JS_DestroyRuntime(m_rt); + } + + JSRuntime* m_rt; + AutoGCRooter* m_rooter; + +private: + +#if ENABLE_SCRIPT_PROFILING + static void* jshook_script(JSContext* UNUSED(cx), JSStackFrame* UNUSED(fp), JSBool before, JSBool* UNUSED(ok), void* closure) + { + if (before) + g_Profiler.StartScript("script invocation"); + else + g_Profiler.Stop(); + + return closure; + } + + static void* jshook_function(JSContext* cx, JSStackFrame* fp, JSBool before, JSBool* UNUSED(ok), void* closure) + { + JSFunction* fn = JS_GetFrameFunction(cx, fp); + if (before) + { + if (fn) + g_Profiler.StartScript(JS_GetFunctionName(fn)); + else + g_Profiler.StartScript("function invocation"); + } + else + g_Profiler.Stop(); + + return closure; + } +#endif + + static void jshook_trace(JSTracer* trc, void* data) + { + ScriptRuntime* m = static_cast(data); + + if (m->m_rooter) + m->m_rooter->Trace(trc); + } +}; + +shared_ptr ScriptInterface::CreateRuntime() +{ + return shared_ptr(new ScriptRuntime); +} + +//////////////////////////////////////////////////////////////// + struct ScriptInterface_impl { - ScriptInterface_impl(const char* nativeScopeName, JSContext* cx); + ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uintN nargs); - JSRuntime* m_rt; // NULL if m_cx is shared; non-NULL if we own m_cx + shared_ptr m_runtime; JSContext* m_cx; JSObject* m_glob; // global scope object JSObject* m_nativeScope; // native function scope object - - AutoGCRooter* m_rooter; }; namespace @@ -202,103 +278,41 @@ JSBool Math_random(JSContext* cx, uintN UNUSED(argc), jsval* vp) } // anonymous namespace -#if ENABLE_SCRIPT_PROFILING -static void* jshook_script(JSContext* UNUSED(cx), JSStackFrame* UNUSED(fp), JSBool before, JSBool* UNUSED(ok), void* closure) -{ - if (before) - g_Profiler.StartScript("script invocation"); - else - g_Profiler.Stop(); - - return closure; -} - -static void* jshook_function(JSContext* cx, JSStackFrame* fp, JSBool before, JSBool* UNUSED(ok), void* closure) -{ - JSFunction* fn = JS_GetFrameFunction(cx, fp); - if (before) - { - if (fn) - g_Profiler.StartScript(JS_GetFunctionName(fn)); - else - g_Profiler.StartScript("function invocation"); - } - else - g_Profiler.Stop(); - - return closure; -} -#endif - -void jshook_trace(JSTracer* trc, void* data) -{ - ScriptInterface_impl* m = static_cast(data); - - if (m->m_rooter) - m->m_rooter->Trace(trc); -} - -ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, JSContext* cx) : - m_rooter(NULL) +ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime) : + m_runtime(runtime) { JSBool ok; - if (cx) - { - m_rt = NULL; - m_cx = cx; - m_glob = JS_GetGlobalObject(m_cx); - } - else - { - m_rt = JS_NewRuntime(RUNTIME_SIZE); - debug_assert(m_rt); // TODO: error handling + m_cx = JS_NewContext(m_runtime->m_rt, STACK_CHUNK_SIZE); + debug_assert(m_cx); -#if ENABLE_SCRIPT_PROFILING - // Profiler isn't thread-safe, so only enable this on the main thread - if (ThreadUtil::IsMainThread()) - { - if (CProfileManager::IsInitialised()) - { - JS_SetExecuteHook(m_rt, jshook_script, this); - JS_SetCallHook(m_rt, jshook_function, this); - } - } -#endif + // For GC debugging: + // JS_SetGCZeal(m_cx, 2); - m_cx = JS_NewContext(m_rt, STACK_CHUNK_SIZE); - debug_assert(m_cx); + JS_SetContextPrivate(m_cx, NULL); - // For GC debugging: - // JS_SetGCZeal(m_cx, 2); + JS_SetErrorReporter(m_cx, ErrorReporter); - JS_SetContextPrivate(m_cx, NULL); + JS_SetOptions(m_cx, JSOPTION_STRICT // "warn on dubious practice" + | JSOPTION_XML // "ECMAScript for XML support: parse as a token" + | JSOPTION_VAROBJFIX // "recommended" (fixes variable scoping) - JS_SetErrorReporter(m_cx, ErrorReporter); + // Enable all the JIT features: +// | JSOPTION_JIT +// | JSOPTION_METHODJIT +// | JSOPTION_PROFILING + ); - JS_SetOptions(m_cx, JSOPTION_STRICT // "warn on dubious practice" - | JSOPTION_XML // "ECMAScript for XML support: parse as a token" - | JSOPTION_VAROBJFIX // "recommended" (fixes variable scoping) + JS_SetVersion(m_cx, JSVERSION_LATEST); - // Enable all the JIT features: -// | JSOPTION_JIT -// | JSOPTION_METHODJIT -// | JSOPTION_PROFILING - ); + // Threadsafe SpiderMonkey requires that we have a request before doing anything much + JS_BeginRequest(m_cx); - JS_SetVersion(m_cx, JSVERSION_LATEST); + m_glob = JS_NewGlobalObject(m_cx, &global_class); + ok = JS_InitStandardClasses(m_cx, m_glob); - JS_SetExtraGCRoots(m_rt, jshook_trace, this); - - // Threadsafe SpiderMonkey requires that we have a request before doing anything much - JS_BeginRequest(m_cx); - - m_glob = JS_NewGlobalObject(m_cx, &global_class); - ok = JS_InitStandardClasses(m_cx, m_glob); - - JS_DefineProperty(m_cx, m_glob, "global", OBJECT_TO_JSVAL(m_glob), NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY - | JSPROP_PERMANENT); - } + JS_DefineProperty(m_cx, m_glob, "global", OBJECT_TO_JSVAL(m_glob), NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY + | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); @@ -311,12 +325,8 @@ ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, JSContex ScriptInterface_impl::~ScriptInterface_impl() { - if (m_rt) // if we own the context: - { - JS_EndRequest(m_cx); - JS_DestroyContext(m_cx); - JS_DestroyRuntime(m_rt); - } + JS_EndRequest(m_cx); + JS_DestroyContext(m_cx); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uintN nargs) @@ -324,8 +334,8 @@ void ScriptInterface_impl::Register(const char* name, JSNative fptr, uintN nargs JS_DefineFunction(m_cx, m_nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); } -ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName) : - m(new ScriptInterface_impl(nativeScopeName, NULL)) +ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime) : + m(new ScriptInterface_impl(nativeScopeName, runtime)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) @@ -391,14 +401,13 @@ JSContext* ScriptInterface::GetContext() const JSRuntime* ScriptInterface::GetRuntime() const { - return m->m_rt; + return m->m_runtime->m_rt; } AutoGCRooter* ScriptInterface::ReplaceAutoGCRooter(AutoGCRooter* rooter) { - debug_assert(m->m_rt); // this class must own the runtime, else the rooter won't work - AutoGCRooter* ret = m->m_rooter; - m->m_rooter = rooter; + AutoGCRooter* ret = m->m_runtime->m_rooter; + m->m_runtime->m_rooter = rooter; return ret; } @@ -821,9 +830,9 @@ void ScriptInterface::DumpHeap() #ifdef DEBUG JS_DumpHeap(m->m_cx, stderr, NULL, 0, NULL, (size_t)-1, NULL); #endif - fprintf(stderr, "# Bytes allocated: %d\n", JS_GetGCParameter(m->m_rt, JSGC_BYTES)); + fprintf(stderr, "# Bytes allocated: %d\n", JS_GetGCParameter(GetRuntime(), JSGC_BYTES)); JS_GC(m->m_cx); - fprintf(stderr, "# Bytes allocated after GC: %d\n", JS_GetGCParameter(m->m_rt, JSGC_BYTES)); + fprintf(stderr, "# Bytes allocated after GC: %d\n", JS_GetGCParameter(GetRuntime(), JSGC_BYTES)); } void ScriptInterface::MaybeGC() @@ -948,3 +957,36 @@ jsval ScriptInterface::CloneValueFromOtherContext(ScriptInterface& otherContext, ValueCloner cloner(otherContext, *this); return cloner.GetOrClone(val); } + +ScriptInterface::StructuredClone::StructuredClone() : + m_Data(NULL), m_Size(0) +{ +} + +ScriptInterface::StructuredClone::~StructuredClone() +{ + if (m_Data) + js_free(m_Data); +} + +shared_ptr ScriptInterface::WriteStructuredClone(jsval v) +{ + uint64* data = NULL; + size_t nbytes = 0; + if (!JS_WriteStructuredClone(m->m_cx, v, &data, &nbytes)) + return shared_ptr(); + // TODO: should we have better error handling? + // Currently we'll probably continue and then crash in ReadStructuredClone + + shared_ptr ret (new StructuredClone); + ret->m_Data = data; + ret->m_Size = nbytes; + return ret; +} + +jsval ScriptInterface::ReadStructuredClone(const shared_ptr& ptr) +{ + jsval ret = JSVAL_VOID; + JS_ReadStructuredClone(m->m_cx, ptr->m_Data, ptr->m_Size, &ret); + return ret; +} diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index 6fc6471f22..6116fccb23 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/source/scriptinterface/ScriptInterface.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -48,6 +48,8 @@ namespace boost { class rand48; } struct ScriptInterface_impl; +class ScriptRuntime; + /** * Abstraction around a SpiderMonkey JSContext. * @@ -60,14 +62,20 @@ class ScriptInterface { public: + /** + * Returns a runtime, which can used to initialise any number of + * ScriptInterfaces contexts. Values created in one context may be used + * in any other context from the same runtime (but not any other runtime). + * Each runtime should only ever be used on a single thread. + */ + static shared_ptr CreateRuntime(); + /** * Constructor. * @param nativeScopeName Name of global object that functions (via RegisterFunction) will * be placed into, as a scoping mechanism; typically "Engine" - * @param cx NULL if the object should create and manage its own context; otherwise - * an existing context which it will share */ - ScriptInterface(const char* nativeScopeName, const char* debugName = "Unknown"); + ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime); ~ScriptInterface(); @@ -263,6 +271,26 @@ public: void MaybeGC(); + /** + * Structured clones are a way to serialize 'simple' JS values into a buffer + * that can safely be passed between contexts and runtimes and threads. + * A StructuredClone can be stored and read multiple times if desired. + * We wrap them in shared_ptr so memory management is automatic and + * thread-safe. + */ + class StructuredClone + { + NONCOPYABLE(StructuredClone); + public: + StructuredClone(); + ~StructuredClone(); + uint64* m_Data; + size_t m_Size; + }; + + shared_ptr WriteStructuredClone(jsval v); + jsval ReadStructuredClone(const shared_ptr& ptr); + private: bool CallFunction_(jsval val, const char* name, size_t argc, jsval* argv, jsval& ret); bool Eval_(const char* code, jsval& ret); diff --git a/source/scriptinterface/ScriptVal.h b/source/scriptinterface/ScriptVal.h index 0efa57e322..d992d43d90 100644 --- a/source/scriptinterface/ScriptVal.h +++ b/source/scriptinterface/ScriptVal.h @@ -40,7 +40,7 @@ public: /** * Returns whether the value is JSVAL_VOID. */ - bool undefined() const { return JSVAL_IS_VOID(m_Val); } + bool undefined() const { return JSVAL_IS_VOID(m_Val) ? true : false; } private: jsval m_Val; diff --git a/source/scriptinterface/tests/test_ScriptConversions.h b/source/scriptinterface/tests/test_ScriptConversions.h index 3d2827d8a0..99cded81ad 100644 --- a/source/scriptinterface/tests/test_ScriptConversions.h +++ b/source/scriptinterface/tests/test_ScriptConversions.h @@ -30,7 +30,7 @@ class TestScriptConversions : public CxxTest::TestSuite template void convert_to(const T& value, const std::string& expected) { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); JSContext* cx = script.GetContext(); jsval v1 = ScriptInterface::ToJSVal(cx, value); @@ -46,7 +46,7 @@ class TestScriptConversions : public CxxTest::TestSuite template void roundtrip(const T& value, const char* expected) { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); JSContext* cx = script.GetContext(); jsval v1 = ScriptInterface::ToJSVal(cx, value); @@ -121,7 +121,7 @@ public: void test_integers() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); JSContext* cx = script.GetContext(); TS_ASSERT(JSVAL_IS_INT(ScriptInterface::ToJSVal(cx, 0))); @@ -144,7 +144,7 @@ public: roundtrip(-INFINITY, "-Infinity"); convert_to(NAN, "NaN"); // can't use roundtrip since nan != nan - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); JSContext* cx = script.GetContext(); float f = 0; diff --git a/source/scriptinterface/tests/test_ScriptInterface.h b/source/scriptinterface/tests/test_ScriptInterface.h index 4008631390..e9ff869518 100644 --- a/source/scriptinterface/tests/test_ScriptInterface.h +++ b/source/scriptinterface/tests/test_ScriptInterface.h @@ -29,7 +29,7 @@ class TestScriptInterface : public CxxTest::TestSuite public: void test_loadscript_basic() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); TestLogger logger; TS_ASSERT(script.LoadScript(L"test.js", L"var x = 1+1;")); TS_ASSERT_WSTR_NOT_CONTAINS(logger.GetOutput(), L"JavaScript error"); @@ -38,7 +38,7 @@ public: void test_loadscript_error() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); TestLogger logger; TS_ASSERT(!script.LoadScript(L"test.js", L"1+")); TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"JavaScript error: test.js line 1\nSyntaxError: syntax error"); @@ -46,7 +46,7 @@ public: void test_loadscript_strict_warning() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); TestLogger logger; TS_ASSERT(script.LoadScript(L"test.js", L"1+1;")); TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"JavaScript warning: test.js line 1\nuseless expression"); @@ -54,7 +54,7 @@ public: void test_loadscript_strict_error() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); TestLogger logger; TS_ASSERT(!script.LoadScript(L"test.js", L"with(1){}")); TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"JavaScript error: test.js line 1\nSyntaxError: strict mode code may not contain \'with\' statements"); @@ -62,8 +62,8 @@ public: void test_clone_basic() { - ScriptInterface script1("Test"); - ScriptInterface script2("Test"); + ScriptInterface script1("Test", "Test", ScriptInterface::CreateRuntime()); + ScriptInterface script2("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj1; TS_ASSERT(script1.Eval("({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", obj1)); @@ -79,8 +79,8 @@ public: { // The tests should be run with JS_SetGCZeal so this can try to find GC bugs - ScriptInterface script1("Test"); - ScriptInterface script2("Test"); + ScriptInterface script1("Test", "Test", ScriptInterface::CreateRuntime()); + ScriptInterface script2("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj1; TS_ASSERT(script1.Eval("var s = '?'; var v = ({get x() { return 123 }, 'y': {'w':{get z() { delete v.y; delete v.n; v = null; s += s; return 4 }}}, 'n': 100}); v", obj1)); @@ -94,8 +94,8 @@ public: void test_clone_cyclic() { - ScriptInterface script1("Test"); - ScriptInterface script2("Test"); + ScriptInterface script1("Test", "Test", ScriptInterface::CreateRuntime()); + ScriptInterface script2("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj1; TS_ASSERT(script1.Eval("var x = []; x[0] = x; ({'a': x, 'b': x})", obj1)); @@ -109,7 +109,7 @@ public: void test_random() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); double d1, d2; TS_ASSERT(script.Eval("Math.random()", d1)); @@ -129,7 +129,7 @@ public: void test_json() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::string input = "({'x':1,'z':[2,'3\\u263A\\ud800'],\"y\":true})"; CScriptValRooted val; diff --git a/source/scriptinterface/tests/test_ScriptVal.h b/source/scriptinterface/tests/test_ScriptVal.h index 915d4b6200..7d1a7271c4 100644 --- a/source/scriptinterface/tests/test_ScriptVal.h +++ b/source/scriptinterface/tests/test_ScriptVal.h @@ -27,7 +27,7 @@ class TestScriptVal : public CxxTest::TestSuite public: void test_rooting() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); JSContext* cx = script.GetContext(); JSObject* obj = JS_NewObject(cx, NULL, NULL, NULL); diff --git a/source/simulation2/TypeList.h b/source/simulation2/TypeList.h index 00af7da37a..cda8b3febd 100644 --- a/source/simulation2/TypeList.h +++ b/source/simulation2/TypeList.h @@ -63,9 +63,6 @@ COMPONENT(AIInterfaceScripted) INTERFACE(AIManager) COMPONENT(AIManager) -INTERFACE(AIProxy) -COMPONENT(AIProxyScripted) - INTERFACE(CommandQueue) COMPONENT(CommandQueue) diff --git a/source/simulation2/components/CCmpAIManager.cpp b/source/simulation2/components/CCmpAIManager.cpp index 6b67f1e36b..fbe61791f6 100644 --- a/source/simulation2/components/CCmpAIManager.cpp +++ b/source/simulation2/components/CCmpAIManager.cpp @@ -24,7 +24,6 @@ #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "simulation2/components/ICmpAIInterface.h" -#include "simulation2/components/ICmpAIProxy.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/serialization/DebugSerializer.h" @@ -50,6 +49,8 @@ * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * + * JS values are passed between the game and AI threads using ScriptInterface::StructuredClone. + * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. @@ -58,37 +59,157 @@ class CAIWorker { private: - struct SAIPlayer + class CAIPlayer { - std::wstring aiName; - player_id_t player; - CScriptValRooted obj; - }; + NONCOPYABLE(CAIPlayer); + public: + CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, + const shared_ptr& runtime, boost::rand48& rng) : + m_Worker(worker), m_AIName(aiName), m_Player(player), m_ScriptInterface("Engine", "AI", runtime) + { + m_ScriptInterface.SetCallbackData(static_cast (this)); - struct SCommands - { - player_id_t player; - std::vector commands; + m_ScriptInterface.ReplaceNondeterministicFunctions(rng); + + m_ScriptInterface.RegisterFunction("IncludeModule"); + m_ScriptInterface.RegisterFunction("PostCommand"); + } + + ~CAIPlayer() + { + // Clean up rooted objects before destroying their script context + m_Obj = CScriptValRooted(); + } + + static void IncludeModule(void* cbdata, std::wstring name) + { + CAIPlayer* self = static_cast (cbdata); + + self->LoadScripts(name); + } + + static void PostCommand(void* cbdata, CScriptValRooted cmd) + { + CAIPlayer* self = static_cast (cbdata); + + self->m_Commands.push_back(self->m_ScriptInterface.WriteStructuredClone(cmd.get())); + } + + bool LoadScripts(const std::wstring& moduleName) + { + // Ignore modules that are already loaded + if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) + return true; + + // Mark this as loaded, to prevent it recursively loading itself + m_LoadedModules.insert(moduleName); + + // Load and execute *.js + VfsPaths pathnames; + fs_util::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames); + for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) + { + if (!m_ScriptInterface.LoadGlobalScriptFile(*it)) + { + LOGERROR(L"Failed to load script %ls", it->string().c_str()); + return false; + } + } + + return true; + } + + bool Initialise(bool callConstructor) + { + if (!LoadScripts(m_AIName)) + return false; + + std::wstring path = L"simulation/ai/" + m_AIName + L"/data.json"; + CScriptValRooted metadata = m_Worker.LoadMetadata(path); + if (metadata.uninitialised()) + { + LOGERROR(L"Failed to create AI player: can't find %ls", path.c_str()); + return false; + } + + // Get the constructor name from the metadata + std::string constructor; + if (!m_ScriptInterface.GetProperty(metadata.get(), "constructor", constructor)) + { + LOGERROR(L"Failed to create AI player: %ls: missing 'constructor'", path.c_str()); + return false; + } + + // Get the constructor function from the loaded scripts + CScriptVal ctor; + if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), constructor.c_str(), ctor) + || ctor.undefined()) + { + LOGERROR(L"Failed to create AI player: %ls: can't find constructor '%hs'", path.c_str(), constructor.c_str()); + return false; + } + + CScriptVal obj; + + if (callConstructor) + { + // Set up the data to pass as the constructor argument + CScriptVal settings; + m_ScriptInterface.Eval(L"({})", settings); + m_ScriptInterface.SetProperty(settings.get(), "player", m_Player, false); + m_ScriptInterface.SetProperty(settings.get(), "templates", m_Worker.m_EntityTemplates, false); + + obj = m_ScriptInterface.CallConstructor(ctor.get(), settings.get()); + } + else + { + // For deserialization, we want to create the object with the correct prototype + // but don't want to actually run the constructor again + obj = m_ScriptInterface.NewObjectFromConstructor(ctor.get()); + } + + if (obj.undefined()) + { + LOGERROR(L"Failed to create AI player: %ls: error calling constructor '%hs'", path.c_str(), constructor.c_str()); + return false; + } + + m_Obj = CScriptValRooted(m_ScriptInterface.GetContext(), obj); + return true; + } + + void Run(CScriptVal state) + { + m_Commands.clear(); + m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state); + } + + CAIWorker& m_Worker; + std::wstring m_AIName; + player_id_t m_Player; + + ScriptInterface m_ScriptInterface; + CScriptValRooted m_Obj; + std::vector > m_Commands; + std::set m_LoadedModules; }; public: - struct SReturnedCommands + struct SCommandSets { player_id_t player; - std::vector commands; + std::vector > commands; }; CAIWorker() : - m_ScriptInterface("Engine", "AI"), - m_CommandsComputed(true), - m_CurrentlyComputingPlayer(-1) + m_ScriptRuntime(ScriptInterface::CreateRuntime()), + m_ScriptInterface("Engine", "AI", m_ScriptRuntime), + m_CommandsComputed(true) { m_ScriptInterface.SetCallbackData(static_cast (this)); // TODO: ought to seed the RNG (in a network-synchronised way) before we use it m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG); - - m_ScriptInterface.RegisterFunction("PostCommand"); } ~CAIWorker() @@ -97,70 +218,20 @@ public: m_EntityTemplates = CScriptValRooted(); m_PlayerMetadata.clear(); m_Players.clear(); - m_Commands.clear(); } bool AddPlayer(const std::wstring& aiName, player_id_t player, bool callConstructor) { - std::wstring path = L"simulation/ai/" + aiName + L"/data.json"; - CScriptValRooted metadata = LoadPlayerFiles(aiName, path); - if (metadata.uninitialised()) - { - LOGERROR(L"Failed to create AI player: can't find %ls", path.c_str()); + shared_ptr ai(new CAIPlayer(*this, aiName, player, m_ScriptRuntime, m_RNG)); + if (!ai->Initialise(callConstructor)) return false; - } - // Get the constructor name from the metadata - std::string constructor; - if (!m_ScriptInterface.GetProperty(metadata.get(), "constructor", constructor)) - { - LOGERROR(L"Failed to create AI player: %ls: missing 'constructor'", path.c_str()); - return false; - } - - // Get the constructor function from the loaded scripts - CScriptVal ctor; - if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), constructor.c_str(), ctor) - || ctor.undefined()) - { - LOGERROR(L"Failed to create AI player: %ls: can't find constructor '%hs'", path.c_str(), constructor.c_str()); - return false; - } - - CScriptVal obj; - - if (callConstructor) - { - // Set up the data to pass as the constructor argument - CScriptVal settings; - m_ScriptInterface.Eval(L"({})", settings); - m_ScriptInterface.SetProperty(settings.get(), "player", player, false); - m_ScriptInterface.SetProperty(settings.get(), "templates", m_EntityTemplates, false); - - obj = m_ScriptInterface.CallConstructor(ctor.get(), settings.get()); - } - else - { - // For deserialization, we want to create the object with the correct prototype - // but don't want to actually run the constructor again - obj = m_ScriptInterface.NewObjectFromConstructor(ctor.get()); - } - - if (obj.undefined()) - { - LOGERROR(L"Failed to create AI player: %ls: error calling constructor '%hs'", path.c_str(), constructor.c_str()); - return false; - } - - SAIPlayer ai; - ai.aiName = aiName; - ai.player = player; - ai.obj = CScriptValRooted(m_ScriptInterface.GetContext(), obj); m_Players.push_back(ai); + return true; } - void StartComputation(const std::string& gameState) + void StartComputation(const shared_ptr& gameState) { debug_assert(m_CommandsComputed); @@ -178,26 +249,16 @@ public: } } - void GetCommands(std::vector& commands) + void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); - commands.resize(m_Commands.size()); - for (size_t i = 0; i < m_Commands.size(); ++i) + commands.resize(m_Players.size()); + for (size_t i = 0; i < m_Players.size(); ++i) { - commands[i].player = m_Commands[i].player; - commands[i].commands.resize(m_Commands[i].commands.size()); - for (size_t j = 0; j < m_Commands[i].commands.size(); ++j) - { - // Serialize the returned command, so that it's safe to transfer - // across threads (in the future when we actually run AI in a - // background thread) - std::stringstream stream; - CStdSerializer serializer(m_ScriptInterface, stream); - serializer.ScriptVal("command", m_Commands[i].commands[j]); - commands[i].commands[j] = stream.str(); - } + commands[i].player = m_Players[i]->m_Player; + commands[i].commands = m_Players[i]->m_Commands; } } @@ -239,17 +300,16 @@ public: for (size_t i = 0; i < m_Players.size(); ++i) { - serializer.String("name", m_Players[i].aiName, 0, 256); - serializer.NumberI32_Unbounded("player", m_Players[i].player); - serializer.ScriptVal("data", m_Players[i].obj); - } + serializer.String("name", m_Players[i]->m_AIName, 0, 256); + serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); + serializer.ScriptVal("data", m_Players[i]->m_Obj); - serializer.NumberU32_Unbounded("num ai commands", m_Commands.size()); - - for (size_t i = 0; i < m_Commands.size(); ++i) - { - serializer.NumberI32_Unbounded("player", m_Commands[i].player); - SerializeVector()(serializer, "commands", m_Commands[i].commands); + serializer.NumberU32_Unbounded("num commands", m_Players[i]->m_Commands.size()); + for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) + { + CScriptVal val = m_ScriptInterface.ReadStructuredClone(m_Players[i]->m_Commands[j]); + serializer.ScriptVal("command", val); + } } } @@ -261,7 +321,6 @@ public: m_PlayerMetadata.clear(); m_Players.clear(); - m_Commands.clear(); uint32_t numAis; deserializer.NumberU32_Unbounded("num ais", numAis); @@ -277,37 +336,27 @@ public: // Use ScriptObjectAppend so we don't lose the carefully-constructed // prototype/parent of this object - deserializer.ScriptObjectAppend("data", m_Players.back().obj.getRef()); - } + deserializer.ScriptObjectAppend("data", m_Players.back()->m_Obj.getRef()); - uint32_t numCommands; - deserializer.NumberU32_Unbounded("num ai commands", numCommands); - - m_Commands.resize(numCommands); - for (size_t i = 0; i < numCommands; ++i) - { - deserializer.NumberI32_Unbounded("player", m_Commands[i].player); - SerializeVector()(deserializer, "commands", m_Commands[i].commands); + uint32_t numCommands; + deserializer.NumberU32_Unbounded("num commands", numCommands); + m_Players.back()->m_Commands.reserve(numCommands); + for (size_t j = 0; j < numCommands; ++j) + { + CScriptVal val; + deserializer.ScriptVal("command", val); + m_Players.back()->m_Commands.push_back(m_ScriptInterface.WriteStructuredClone(val.get())); + } } } private: - CScriptValRooted LoadPlayerFiles(const std::wstring& aiName, const std::wstring& path) + CScriptValRooted LoadMetadata(const std::wstring& path) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata m_PlayerMetadata[path] = m_ScriptInterface.ReadJSONFile(path); - - // TODO: includes - - // Load and execute *.js - VfsPaths pathnames; - fs_util::GetPathnames(g_VFS, L"simulation/ai/" + aiName + L"/", L"*.js", pathnames); - for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) - { - m_ScriptInterface.LoadGlobalScriptFile(*it); - } } return m_PlayerMetadata[path]; @@ -317,55 +366,30 @@ private: { PROFILE("AI compute"); - m_Commands.clear(); - // Deserialize the game state, to pass to the AI's HandleMessage - CScriptVal state; + CScriptVal state = m_ScriptInterface.ReadStructuredClone(m_GameState); - { -// TIMER(L"deserialize AI game state"); - std::stringstream stream(m_GameState); - CStdDeserializer deserializer(m_ScriptInterface, stream); - deserializer.ScriptVal("state", state); - } - - m_ScriptInterface.FreezeObject(state.get(), true); - - m_Commands.resize(m_Players.size()); + // It would be nice to do + // m_ScriptInterface.FreezeObject(state.get(), true); + // to prevent AI scripts accidentally modifying the state and + // affecting other AI scripts they share it with. But the performance + // cost is far too high, so we won't do that. for (size_t i = 0; i < m_Players.size(); ++i) - { - m_Commands[i].player = m_Players[i].player; - - m_CurrentlyComputingPlayer = i; - m_ScriptInterface.CallFunctionVoid(m_Players[i].obj.get(), "HandleMessage", state); - // (This script will probably call PostCommand) - } - - m_CurrentlyComputingPlayer = -1; - } - - static void PostCommand(void* cbdata, CScriptValRooted cmd) - { - CAIWorker* self = static_cast (cbdata); - - debug_assert(self->m_CurrentlyComputingPlayer >= 0); // called outside of PerformComputation somehow - - self->m_Commands[self->m_CurrentlyComputingPlayer].commands.push_back(cmd); + m_Players[i]->Run(state); } + shared_ptr m_ScriptRuntime; ScriptInterface m_ScriptInterface; boost::rand48 m_RNG; CScriptValRooted m_EntityTemplates; std::map m_PlayerMetadata; - std::vector m_Players; + std::vector > m_Players; // use shared_ptr just to avoid copying - std::string m_GameState; + shared_ptr m_GameState; bool m_CommandsComputed; - std::vector m_Commands; - int m_CurrentlyComputingPlayer; // used so PostCommand knows what player the command is for }; @@ -424,45 +448,17 @@ public: CmpPtr cmpAIInterface(GetSimContext(), SYSTEM_ENTITY); debug_assert(!cmpAIInterface.null()); - // Get most of the game state from AIInterface + // Get the game state from AIInterface CScriptVal state = cmpAIInterface->GetRepresentation(); - // Get entity state from each entity: - - CScriptVal entities; - scriptInterface.Eval(L"({})", entities); - - const std::map& ents = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_AIProxy); - - for (std::map::const_iterator it = ents.begin(); it != ents.end(); ++it) - { - // Skip local entities - if (ENTITY_IS_LOCAL(it->first)) - continue; - - CScriptVal rep = static_cast(it->second)->GetRepresentation(); - - scriptInterface.SetPropertyInt(entities.get(), it->first, rep, true); - } - - // Add the entities state into the object returned by AIInterface - scriptInterface.SetProperty(state.get(), "entities", entities, true); - - // Serialize the state representation, so that it's safe to transfer - // across threads (in the future when we actually run AI in a - // background thread) - std::stringstream stream; - CStdSerializer serializer(scriptInterface, stream); - serializer.ScriptVal("state", state); - - m_Worker.StartComputation(stream.str()); + m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state.get())); } virtual void PushCommands() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); - std::vector commands; + std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSimContext(), SYSTEM_ENTITY); @@ -473,11 +469,8 @@ public: { for (size_t j = 0; j < commands[i].commands.size(); ++j) { - std::stringstream stream(commands[i].commands[j]); - CStdDeserializer deserializer(scriptInterface, stream); - CScriptVal cmd; - deserializer.ScriptVal("command", cmd); - cmpCommandQueue->PushLocalCommand(commands[i].player, cmd); + cmpCommandQueue->PushLocalCommand(commands[i].player, + scriptInterface.ReadStructuredClone(commands[i].commands[j])); } } } diff --git a/source/simulation2/components/CCmpTerrain.cpp b/source/simulation2/components/CCmpTerrain.cpp index a5f8bdc883..b4977f68d6 100644 --- a/source/simulation2/components/CCmpTerrain.cpp +++ b/source/simulation2/components/CCmpTerrain.cpp @@ -69,6 +69,8 @@ public: virtual entity_pos_t GetGroundLevel(entity_pos_t x, entity_pos_t z) { + // TODO: this can crash if the terrain heightmap isn't initialised yet + return m_Terrain->GetExactGroundLevelFixed(x, z); } diff --git a/source/simulation2/components/ICmpAIProxy.cpp b/source/simulation2/components/ICmpAIProxy.cpp deleted file mode 100644 index 20707ea026..0000000000 --- a/source/simulation2/components/ICmpAIProxy.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* Copyright (C) 2011 Wildfire Games. - * This file is part of 0 A.D. - * - * 0 A.D. is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * 0 A.D. is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with 0 A.D. If not, see . - */ - -#include "precompiled.h" - -#include "ICmpAIProxy.h" - -#include "simulation2/system/InterfaceScripted.h" -#include "simulation2/scripting/ScriptComponent.h" - -BEGIN_INTERFACE_WRAPPER(AIProxy) -END_INTERFACE_WRAPPER(AIProxy) - -class CCmpAIProxyScripted : public ICmpAIProxy -{ -public: - DEFAULT_SCRIPT_WRAPPER(AIProxyScripted) - - virtual CScriptVal GetRepresentation() - { - return m_Script.Call ("GetRepresentation"); - } -}; - -REGISTER_COMPONENT_SCRIPT_WRAPPER(AIProxyScripted) diff --git a/source/simulation2/components/ICmpAIProxy.h b/source/simulation2/components/ICmpAIProxy.h deleted file mode 100644 index cc570c8c80..0000000000 --- a/source/simulation2/components/ICmpAIProxy.h +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright (C) 2011 Wildfire Games. - * This file is part of 0 A.D. - * - * 0 A.D. is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * 0 A.D. is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with 0 A.D. If not, see . - */ - -#ifndef INCLUDED_ICMPAIPROXY -#define INCLUDED_ICMPAIPROXY - -#include "simulation2/system/Interface.h" - -class ICmpAIProxy : public IComponent -{ -public: - /** - * Returns a script object that represents the current entity state, - * to be passed to AI scripts. - */ - virtual CScriptVal GetRepresentation() = 0; - - DECLARE_INTERFACE_TYPE(AIProxy) -}; - -#endif // INCLUDED_ICMPAIPROXY diff --git a/source/simulation2/components/ICmpPosition.cpp b/source/simulation2/components/ICmpPosition.cpp index e0da2a8224..a5d2d651d7 100644 --- a/source/simulation2/components/ICmpPosition.cpp +++ b/source/simulation2/components/ICmpPosition.cpp @@ -30,6 +30,7 @@ DEFINE_INTERFACE_METHOD_1("SetHeightOffset", void, ICmpPosition, SetHeightOffset DEFINE_INTERFACE_METHOD_0("GetHeightOffset", entity_pos_t, ICmpPosition, GetHeightOffset) DEFINE_INTERFACE_METHOD_0("IsFloating", bool, ICmpPosition, IsFloating) DEFINE_INTERFACE_METHOD_0("GetPosition", CFixedVector3D, ICmpPosition, GetPosition) +DEFINE_INTERFACE_METHOD_0("GetPosition2D", CFixedVector2D, ICmpPosition, GetPosition2D) DEFINE_INTERFACE_METHOD_1("SetYRotation", void, ICmpPosition, SetYRotation, entity_angle_t) DEFINE_INTERFACE_METHOD_2("SetXZRotation", void, ICmpPosition, SetXZRotation, entity_angle_t, entity_angle_t) DEFINE_INTERFACE_METHOD_0("GetRotation", CFixedVector3D, ICmpPosition, GetRotation) diff --git a/source/simulation2/scripting/EngineScriptConversions.cpp b/source/simulation2/scripting/EngineScriptConversions.cpp index 4ce007fcdf..9378bcd980 100644 --- a/source/simulation2/scripting/EngineScriptConversions.cpp +++ b/source/simulation2/scripting/EngineScriptConversions.cpp @@ -192,3 +192,18 @@ template<> bool ScriptInterface::FromJSVal(JSContext* cx, jsval return true; } + +template<> jsval ScriptInterface::ToJSVal(JSContext* cx, const CFixedVector2D& val) +{ + JSObject* obj = JS_NewObject(cx, NULL, NULL, NULL); + if (!obj) + return JSVAL_VOID; + + jsval x = ToJSVal(cx, val.X); + jsval y = ToJSVal(cx, val.Y); + + JS_SetProperty(cx, obj, "x", &x); + JS_SetProperty(cx, obj, "y", &y); + + return OBJECT_TO_JSVAL(obj); +} diff --git a/source/simulation2/scripting/MessageTypeConversions.cpp b/source/simulation2/scripting/MessageTypeConversions.cpp index 8491ceb34d..ccbf008030 100644 --- a/source/simulation2/scripting/MessageTypeConversions.cpp +++ b/source/simulation2/scripting/MessageTypeConversions.cpp @@ -172,14 +172,26 @@ CMessage* CMessageOwnershipChanged::FromJSVal(ScriptInterface& scriptInterface, //////////////////////////////// -jsval CMessagePositionChanged::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const +jsval CMessagePositionChanged::ToJSVal(ScriptInterface& scriptInterface) const { - return JSVAL_VOID; + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(entity); + SET_MSG_PROPERTY(inWorld); + SET_MSG_PROPERTY(x); + SET_MSG_PROPERTY(z); + SET_MSG_PROPERTY(a); + return OBJECT_TO_JSVAL(obj); } -CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) +CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { - return NULL; + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(entity_id_t, entity); + GET_MSG_PROPERTY(bool, inWorld); + GET_MSG_PROPERTY(entity_pos_t, x); + GET_MSG_PROPERTY(entity_pos_t, z); + GET_MSG_PROPERTY(entity_angle_t, a); + return new CMessagePositionChanged(entity, inWorld, x, z, a); } //////////////////////////////// diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index ac05518109..d1eb4c1acc 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -52,7 +52,8 @@ public: }; CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFunctions) : - m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation"), + m_NextScriptComponentTypeId(CID__LastNative), + m_ScriptInterface("Engine", "Simulation", ScriptInterface::CreateRuntime()), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); @@ -72,6 +73,7 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti m_ScriptInterface.RegisterFunction ("RegisterGlobal"); m_ScriptInterface.RegisterFunction ("QueryInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface"); + m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface"); m_ScriptInterface.RegisterFunction ("PostMessage"); m_ScriptInterface.RegisterFunction ("BroadcastMessage"); m_ScriptInterface.RegisterFunction ("AddEntity"); @@ -349,7 +351,18 @@ std::vector CComponentManager::Script_GetEntitiesWithInterface(void* cbdata std::vector ret; const std::map& ents = componentManager->GetEntitiesWithInterface(iid); for (std::map::const_iterator it = ents.begin(); it != ents.end(); ++it) - ret.push_back(it->first); + ret.push_back(it->first); // TODO: maybe we should exclude local entities + return ret; +} + +std::vector CComponentManager::Script_GetComponentsWithInterface(void* cbdata, int iid) +{ + CComponentManager* componentManager = static_cast (cbdata); + + std::vector ret; + const std::map& ents = componentManager->GetEntitiesWithInterface(iid); + for (std::map::const_iterator it = ents.begin(); it != ents.end(); ++it) + ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index 63b56171d4..61ea6cf9cf 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -221,6 +221,7 @@ private: static void Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value); static IComponent* Script_QueryInterface(void* cbdata, int ent, int iid); static std::vector Script_GetEntitiesWithInterface(void* cbdata, int iid); + static std::vector Script_GetComponentsWithInterface(void* cbdata, int iid); static void Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data); static void Script_BroadcastMessage(void* cbdata, int mtid, CScriptVal data); static int Script_AddEntity(void* cbdata, std::string templateName); diff --git a/source/simulation2/tests/test_Serializer.h b/source/simulation2/tests/test_Serializer.h index ae0e826e8f..7d1b0e8ef2 100644 --- a/source/simulation2/tests/test_Serializer.h +++ b/source/simulation2/tests/test_Serializer.h @@ -70,7 +70,7 @@ public: void test_Debug_basic() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); @@ -81,7 +81,7 @@ public: void test_Debug_floats() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); @@ -112,7 +112,7 @@ public: void test_Debug_types() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CDebugSerializer serialize(script, stream); @@ -140,7 +140,7 @@ public: void test_Std_basic() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CStdSerializer serialize(script, stream); @@ -166,7 +166,7 @@ public: void test_Std_types() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CStdSerializer serialize(script, stream); @@ -223,7 +223,7 @@ public: void test_Hash_basic() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); @@ -237,7 +237,7 @@ public: void test_bounds() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); @@ -250,7 +250,7 @@ public: void test_script_basic() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj; TS_ASSERT(script.Eval("({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", obj)); @@ -309,7 +309,7 @@ public: void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL) { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj; TSM_ASSERT(msg, script.Eval(input, obj)); @@ -392,7 +392,7 @@ public: void test_script_exceptions() { - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj; std::stringstream stream; @@ -427,7 +427,7 @@ public: { const char* input = "var x = {}; for (var i=0;i<256;++i) x[i]=Math.pow(i, 2); x"; - ScriptInterface script("Test"); + ScriptInterface script("Test", "Test", ScriptInterface::CreateRuntime()); CScriptVal obj; TS_ASSERT(script.Eval(input, obj));