Add AI script code to provide a cleaner API around the engine interface.

Handle AIProxy entirely through scripts.
Support structured clones of script values.
Improve performance.
Support multiple script contexts sharing a runtime.
Use a separate context per AI player.

This was SVN commit r8866.
This commit is contained in:
Ykkrosh 2011-01-15 23:35:20 +00:00
parent dd501b2a5a
commit f39f279132
31 changed files with 834 additions and 434 deletions

View File

@ -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});
};

View File

@ -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;
},
};

View File

@ -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;
};

View File

@ -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++;

View File

@ -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;
};

View File

@ -3,18 +3,161 @@ function AIProxy() {}
AIProxy.prototype.Schema =
"<empty/>";
/**
* 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);

View File

@ -22,7 +22,7 @@ GarrisonHolder.prototype.Schema =
*/
GarrisonHolder.prototype.Init = function()
{
//Garrisoned Units
// Garrisoned Units
this.entities = [];
this.spaceOccupied = 0;
this.timer = undefined;

View File

@ -0,0 +1 @@
Engine.RegisterInterface("AIProxy");

View File

@ -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)
{

View File

@ -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<CNetClient*> clients;

View File

@ -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());

View File

@ -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");

View File

@ -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();

View File

@ -261,3 +261,9 @@ VECTOR(u32)
VECTOR(std::string)
VECTOR(std::wstring)
VECTOR(CScriptValRooted)
class IComponent;
template<> jsval ScriptInterface::ToJSVal<std::vector<IComponent*> >(JSContext* cx, const std::vector<IComponent*>& val)
{
return ToJSVal_vector(cx, val);
}

View File

@ -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<ScriptRuntime*>(data);
if (m->m_rooter)
m->m_rooter->Trace(trc);
}
};
shared_ptr<ScriptRuntime> ScriptInterface::CreateRuntime()
{
return shared_ptr<ScriptRuntime>(new ScriptRuntime);
}
////////////////////////////////////////////////////////////////
struct ScriptInterface_impl
{
ScriptInterface_impl(const char* nativeScopeName, JSContext* cx);
ScriptInterface_impl(const char* nativeScopeName, const shared_ptr<ScriptRuntime>& 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<ScriptRuntime> 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<ScriptInterface_impl*>(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<ScriptRuntime>& 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<ScriptRuntime>& 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::StructuredClone> ScriptInterface::WriteStructuredClone(jsval v)
{
uint64* data = NULL;
size_t nbytes = 0;
if (!JS_WriteStructuredClone(m->m_cx, v, &data, &nbytes))
return shared_ptr<StructuredClone>();
// TODO: should we have better error handling?
// Currently we'll probably continue and then crash in ReadStructuredClone
shared_ptr<StructuredClone> ret (new StructuredClone);
ret->m_Data = data;
ret->m_Size = nbytes;
return ret;
}
jsval ScriptInterface::ReadStructuredClone(const shared_ptr<ScriptInterface::StructuredClone>& ptr)
{
jsval ret = JSVAL_VOID;
JS_ReadStructuredClone(m->m_cx, ptr->m_Data, ptr->m_Size, &ret);
return ret;
}

View File

@ -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<ScriptRuntime> 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<ScriptRuntime>& 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<StructuredClone> WriteStructuredClone(jsval v);
jsval ReadStructuredClone(const shared_ptr<StructuredClone>& ptr);
private:
bool CallFunction_(jsval val, const char* name, size_t argc, jsval* argv, jsval& ret);
bool Eval_(const char* code, jsval& ret);

View File

@ -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;

View File

@ -30,7 +30,7 @@ class TestScriptConversions : public CxxTest::TestSuite
template <typename T>
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 <typename T>
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<i32>(cx, 0)));
@ -144,7 +144,7 @@ public:
roundtrip<float>(-INFINITY, "-Infinity");
convert_to<float>(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;

View File

@ -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;

View File

@ -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);

View File

@ -63,9 +63,6 @@ COMPONENT(AIInterfaceScripted)
INTERFACE(AIManager)
COMPONENT(AIManager)
INTERFACE(AIProxy)
COMPONENT(AIProxyScripted)
INTERFACE(CommandQueue)
COMPONENT(CommandQueue)

View File

@ -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<ScriptRuntime>& runtime, boost::rand48& rng) :
m_Worker(worker), m_AIName(aiName), m_Player(player), m_ScriptInterface("Engine", "AI", runtime)
{
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
struct SCommands
{
player_id_t player;
std::vector<CScriptValRooted> commands;
m_ScriptInterface.ReplaceNondeterministicFunctions(rng);
m_ScriptInterface.RegisterFunction<void, std::wstring, CAIPlayer::IncludeModule>("IncludeModule");
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIPlayer::PostCommand>("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<CAIPlayer*> (cbdata);
self->LoadScripts(name);
}
static void PostCommand(void* cbdata, CScriptValRooted cmd)
{
CAIPlayer* self = static_cast<CAIPlayer*> (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<shared_ptr<ScriptInterface::StructuredClone> > m_Commands;
std::set<std::wstring> m_LoadedModules;
};
public:
struct SReturnedCommands
struct SCommandSets
{
player_id_t player;
std::vector<std::string> commands;
std::vector<shared_ptr<ScriptInterface::StructuredClone> > 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<void*> (this));
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG);
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIWorker::PostCommand>("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<CAIPlayer> 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<ScriptInterface::StructuredClone>& gameState)
{
debug_assert(m_CommandsComputed);
@ -178,26 +249,16 @@ public:
}
}
void GetCommands(std::vector<SReturnedCommands>& commands)
void GetCommands(std::vector<SCommandSets>& 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<SerializeScriptVal>()(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<SerializeScriptVal>()(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<CAIWorker*> (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<ScriptRuntime> m_ScriptRuntime;
ScriptInterface m_ScriptInterface;
boost::rand48 m_RNG;
CScriptValRooted m_EntityTemplates;
std::map<std::wstring, CScriptValRooted> m_PlayerMetadata;
std::vector<SAIPlayer> m_Players;
std::vector<shared_ptr<CAIPlayer> > m_Players; // use shared_ptr just to avoid copying
std::string m_GameState;
shared_ptr<ScriptInterface::StructuredClone> m_GameState;
bool m_CommandsComputed;
std::vector<SCommands> m_Commands;
int m_CurrentlyComputingPlayer; // used so PostCommand knows what player the command is for
};
@ -424,45 +448,17 @@ public:
CmpPtr<ICmpAIInterface> 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<entity_id_t, IComponent*>& ents = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_AIProxy);
for (std::map<entity_id_t, IComponent*>::const_iterator it = ents.begin(); it != ents.end(); ++it)
{
// Skip local entities
if (ENTITY_IS_LOCAL(it->first))
continue;
CScriptVal rep = static_cast<ICmpAIProxy*>(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<CAIWorker::SReturnedCommands> commands;
std::vector<CAIWorker::SCommandSets> commands;
m_Worker.GetCommands(commands);
CmpPtr<ICmpCommandQueue> 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]));
}
}
}

View File

@ -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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<CScriptVal> ("GetRepresentation");
}
};
REGISTER_COMPONENT_SCRIPT_WRAPPER(AIProxyScripted)

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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

View File

@ -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)

View File

@ -192,3 +192,18 @@ template<> bool ScriptInterface::FromJSVal<CFixedVector2D>(JSContext* cx, jsval
return true;
}
template<> jsval ScriptInterface::ToJSVal<CFixedVector2D>(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);
}

View File

@ -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);
}
////////////////////////////////

View File

@ -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<void, std::string, CScriptVal, CComponentManager::Script_RegisterGlobal> ("RegisterGlobal");
m_ScriptInterface.RegisterFunction<IComponent*, int, int, CComponentManager::Script_QueryInterface> ("QueryInterface");
m_ScriptInterface.RegisterFunction<std::vector<int>, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface");
m_ScriptInterface.RegisterFunction<std::vector<IComponent*>, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface");
m_ScriptInterface.RegisterFunction<void, int, int, CScriptVal, CComponentManager::Script_PostMessage> ("PostMessage");
m_ScriptInterface.RegisterFunction<void, int, CScriptVal, CComponentManager::Script_BroadcastMessage> ("BroadcastMessage");
m_ScriptInterface.RegisterFunction<int, std::string, CComponentManager::Script_AddEntity> ("AddEntity");
@ -349,7 +351,18 @@ std::vector<int> CComponentManager::Script_GetEntitiesWithInterface(void* cbdata
std::vector<int> ret;
const std::map<entity_id_t, IComponent*>& ents = componentManager->GetEntitiesWithInterface(iid);
for (std::map<entity_id_t, IComponent*>::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<IComponent*> CComponentManager::Script_GetComponentsWithInterface(void* cbdata, int iid)
{
CComponentManager* componentManager = static_cast<CComponentManager*> (cbdata);
std::vector<IComponent*> ret;
const std::map<entity_id_t, IComponent*>& ents = componentManager->GetEntitiesWithInterface(iid);
for (std::map<entity_id_t, IComponent*>::const_iterator it = ents.begin(); it != ents.end(); ++it)
ret.push_back(it->second); // TODO: maybe we should exclude local entities
return ret;
}

View File

@ -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<int> Script_GetEntitiesWithInterface(void* cbdata, int iid);
static std::vector<IComponent*> 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);

View File

@ -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));