Pyrogenesis  trunk
How to write components

See the Trac wiki for more documentation about this system.

Defining interfaces in C++

Think of a name for the component. We'll use "Example" in this example; replace it with your chosen name in all the filenames and code samples below.

(If you copy-and-paste from the examples below, be aware that the coding conventions require indentation with tabs, not spaces, so make sure you get it right.)

Create the file simulation2/components/ICmpExample.h:

/* Copyright (C) 2017 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_ICMPEXAMPLE
#define INCLUDED_ICMPEXAMPLE
// ...any other forward declarations and includes you might need...
/**
* Documentation to describe what this interface and its associated component types are
* for, and roughly how they should be used.
*/
class ICmpExample : public IComponent
{
public:
/**
* Documentation for each method.
*/
virtual int DoWhatever(int x, int y) = 0;
// ...
};
#endif // INCLUDED_ICMPEXAMPLE

This defines the interface that C++ code will use to access components.

Create the file simulation2/components/ICmpExample.cpp:

/* Copyright (C) 2021 Wildfire Games.
* ...the usual copyright header...
*/
#include "precompiled.h"
#include "ICmpExample.h"
DEFINE_INTERFACE_METHOD("DoWhatever", ICmpExample, DoWhatever)
// DEFINE_INTERFACE_METHOD for all the other methods too

This defines a JavaScript wrapper, so that scripts can access methods of components implementing that interface. See Interface method script wrappers for details.

This wrapper should only contain methods that are safe to access from simulation scripts: they must not crash (even with invalid or malicious inputs), they must return deterministic results, etc. Methods that are intended for use solely by C++ should not be listed here.

Every interface must define a script wrapper with BEGIN_INTERFACE_WRAPPER, though in some cases they might be empty and not define any methods.

Now update the file simulation2/TypeList.h and add

INTERFACE(Example)

TypeList.h is used for various purposes - it will define the interface ID number IID_Example (in both C++ and JS), and it will hook the new interface into the interface registration system.

Remember to run the update-workspaces script after adding or removing any source files, so that they will be added to the makefiles or VS projects.

Interface method script wrappers

Interface methods are defined with the macro:

DEFINE_INTERFACE_METHOD_NumberOfArguments("MethodName", ReturnType, ICmpExample, MethodName, ArgType0, ArgType1, ...)

corresponding to the C++ method ReturnType ICmpExample::MethodName(ArgType0, ArgType1, ...)

Const methods are defined with this macro:

DEFINE_INTERFACE_METHOD_CONST_NumberOfArguments("MethodName", ReturnType, ICmpExample, MethodName, ArgType0, ArgType1, ...)

corresponding to the C++ const method ReturnType ICmpExample::MethodName(ArgType0, ArgType1, ...) const

For methods exposed to scripts like this, the arguments should be simple types or const references. Check scriptinterface/NativeWrapperDefns.h for which simple types are pass-by-value.

The arguments and return types will be automatically converted between C++ and JS::Values. To do this, ToJSVal<ReturnType> and FromJSVal<ArgTypeN> must be defined (if they haven't already been defined for another method), as described below.

The two MethodNames don't have to be the same - in rare cases you might want to expose it as DoWhatever to scripts but link it to the ICmpExample::DoWhatever_wrapper() method which does some extra conversions or checks or whatever.

There's a small limit to the number of arguments that are currently supported - if you need more, first try to save yourself some pain by using fewer arguments, otherwise you'll need to add a new macro into simulation2/system/InterfaceScripted.h and increase SCRIPT_INTERFACE_MAX_ARGS in scriptinterface/ScriptInterface.h.

Script type conversions

In most cases you can skip this section. But if you define a script-accessible method with new types without having defined conversions, you'll probably get mysterious linker errors that mention ToJSVal or FromJSVal. First, work out where the conversion should be defined. Basic data types (integers, STL containers, etc) go in scriptinterface/ScriptConversions.cpp. Non-basic data types from the game engine typically go in simulation2/scripting/EngineScriptConversions.cpp. (They could go in different files if that turns out to be cleaner - it doesn't matter where they're defined as long as the linker finds them).

To convert from a C++ type T to a JS::Value, define:

template<> void ScriptInterface::ToJSVal<T>(JSContext* cx, JS::MutableHandleValue ret, const T& val)
{
...
}

Use the standard SpiderMonkey JSAPI functions to do the conversion (possibly calling ToJSVal recursively). On error, you should execute ret.setUndefined() and probably report an error message somehow. Be careful about JS garbage collection (don't let it collect the objects you're constructing before you return them).

To convert from a JS::Value to a C++ type T, define:

template<> bool ScriptInterface::FromJSVal<T>(JSContext* cx, JS::HandleValue v, T& out)
{
...
}

On error, return false (doesn't matter what you do with out). On success, return true and put the value in out. Still need to be careful about garbage collection (v is rooted, but it might have getters that execute arbitrary code and return unrooted values when you access properties, so don't let them be collected before you've finished using them).

Defining component types in C++

Now we want to implement the Example interface. We need a name for the component type - if there's only ever going to be one implementation of the interface, we might as well call it Example too. If there's going to be more than one, they should have distinct names like ExampleStatic and ExampleMobile etc.

Create simulation2/components/CCmpExample.cpp:

/* Copyright (C) 2017 Wildfire Games.
* ...the usual copyright header...
*/
#include "precompiled.h"
#include "ICmpExample.h"
// ... any other includes needed ...
class CCmpExample : public ICmpExample
{
public:
static void ClassInit(CComponentManager& componentManager)
{
// ...
}
// ... member variables ...
static std::string GetSchema()
{
return "<ref name='anything'/>";
}
virtual void Init(const CParamNode& paramNode)
{
// ...
}
virtual void Deinit()
{
// ...
}
virtual void Serialize(ISerializer& serialize)
{
// ...
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
// ...
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
// ...
}
// ... Implementation of interface functions: ...
virtual int DoWhatever(int x, int y)
{
return x+y;
}
};

The only optional methods are HandleMessage and GetSchema - all others must be defined.

Update the file simulation2/TypeList.h and add:

COMPONENT(Example)

Message handling

First you need to register for all the message types you want to receive, in ClassInit:

static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(CID_Example, MT_Update);
...
}

(CID_Example is derived from the name of the component type, not the name of the interface.)

You can also use SubscribeGloballyToMessageType, to intercept messages sent with PostMessage that are targeted at a different entity. (Typically this is used by components that want to hear about all MT_Destroy messages.)

Then you need to respond to the messages in HandleMessage:

virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update:
{
const CMessageUpdate& msgData = static_cast<const CMessageUpdate&> (msg);
Update(msgData.turnLength); // or whatever processing you want to do
break;
}
}
}

The CMessage structures are defined in simulation2/MessageTypes.h. Be very careful that you're casting msg to the right type.

Component creation

Component type instances go through one of two lifecycles:

Init(paramNode);
// any sequence of HandleMessage and Serialize and interface methods
Deinit();
Deserialize(paramNode, deserialize);
// any sequence of HandleMessage and Serialize and interface methods
Deinit();

The order of Init/Deserialize/Deinit between entities is mostly undefined, so they must not rely on other entities or components already existing; except that the SYSTEM_ENTITY is created before anything else and therefore may be used, and that the components for a single entity will be processed in the order determined by TypeList.h.

In a typical component:

Component XML schemas

The paramNode passed to Init is constructed from XML entity template definition files. Components should define a schema, which is used for several purposes:

GetSchema must return a Relax NG fragment, which will be used to construct a single global schema file. (You can run the game with the -dumpSchema command-line argument to see the schema. Do not forget to also specify the used mods with -mod= <mod>.). The official tutorial describes most of the details of the RNG language.

In simple cases, you would write something like:

static std::string GetSchema()
{
return
"<element name='Name'><text/></element>"
"<element name='Height'><data type='nonNegativeInteger'/></element>"
"<optional>"
"<element name='Eyes'><empty/></element>"
"</optional>";
}
}

i.e. a single string (C++ automatically concatenates the quoted lines) which defines a list of elements, corresponding to an entity template XML file like:

<Example>
<Name>Barney</Name>
<Height>235</Height>
<Eyes/>
</Example>
<!-- ... other components ... -->

In the schema, each <element> has a name and some content. The content will typically be one of:

The <data> elements are native elements, while the <ref> elements are elements added for our engine. These non-native elements allow the definition of an operation that depends on the parent template. Possible operations are "add" and "mul", and can be applied as the example below.

Say the parent template is

<Example>
<Name>Semi-Humanoids</Name>
<Height>9000</Height>
<Eyes/>
</Example>
<!-- ... other components ... -->

and the child template appears like

<Example>
<Name>Barney</Name>
<Height op="add">5</Height>
<Eyes/>
</Example>
<!-- ... other components ... -->

then Barney would have a height of 9005.

Elements can be wrapped in <optional>. Groups of elements can be wrapped in <choice> to allow only one of them. The content of an <element> can be further nested elements, but note that elements may be reordered when loading an entity template: if you specify a sequence of elements it should be wrapped in <interleave>, so the schema checker will ignore reorderings of the sequence.

For early development of a new component, you can set the schema to <ref name='anything'/> to allow any content. If you don't define GetSchema, then the default is <empty/> (i.e. there must be no elements).

System components

System components are global singleton components of the SYSTEM_ENTITY. These are added to it in CComponentManager::AddSystemComponents, and are passed an empty paramNode on Init.

JS system components can be registered using:

Engine.RegisterSystemComponentType(IID_ExampleSystem, "ExampleSystem", ExampleSystem);

Allowing interfaces to be implemented in JS

If we want to allow both C++ and JS implementations of ICmpExample, we need to define a special component type that proxies all the C++ methods to the script. Add the following to ICmpExample.cpp:

// ...
class CCmpExampleScripted : public ICmpExample
{
public:
DEFAULT_SCRIPT_WRAPPER(ExampleScripted)
virtual int DoWhatever(int x, int y)
{
return m_Script.Call<int>("DoWhatever", x, y);
}
};

Then add to TypeList.h:

COMPONENT(ExampleScripted)

m_Script.Call takes the return type as a template argument, then the name of the JS function to call and the list of parameters. You could do extra conversion work before calling the script, if necessary. You need to make sure the types are handled by ToJSVal and FromJSVal (as discussed before) as appropriate.

Defining component types in JS

Now we want a JS implementation of ICmpExample. Think up a new name for this component, like ExampleTwo (but more imaginative). Then write binaries/data/mods/public/simulation/components/ExampleTwo.js:

function ExampleTwo() {}
ExampleTwo.prototype.Schema = "<ref name='anything'/>";
ExampleTwo.prototype.Init = function() {
...
};
ExampleTwo.prototype.Deinit = function() {
...
};
ExampleTwo.prototype.OnUpdate = function(msg) {
...
};
Engine.RegisterComponentType(IID_Example, "ExampleTwo", ExampleTwo);

This uses JS's prototype system to create what is effectively a class, called ExampleTwo. (If you wrote new ExampleTwo(), then JS would construct a new object which inherits from ExampleTwo.prototype, and then would call the ExampleTwo function with this set to the new object. "Inherit" here means that if you read a property (or method) of the object, which is not defined in the object, then it will be read from the prototype instead.)

Engine.RegisterComponentType tells the engine to start using the JS class ExampleTwo, exposed (in template files etc) with the name "ExampleTwo", and implementing the interface ID IID_Example (i.e. the ICmpExample interface).

The Init and Deinit functions are optional. Unlike C++, there are no Serialize/Deserialize functions - each JS component instance is automatically serialized and restored. (This automatic serialization restricts what you can store as properties in the object - e.g. you cannot store function closures, because they're too hard to serialize. This will serialize Strings, numbers, bools, null, undefined, arrays of serializable values whose property names are purely numeric, objects whose properties are serializable values. Cyclic structures are allowed.)

Instead of ClassInit and HandleMessage, you simply add functions of the form OnMessageType. (If you want the equivalent of SubscribeGloballyToMessageType, then use OnGlobalMessageType instead.) When you call RegisterComponentType, it will find all such functions and automatically subscribe to the messages. The msg parameter is usually a straightforward mapping of the relevant CMessage class onto a JS object (e.g. OnUpdate can read msg.turnLength).

Defining interface types in JS

If an interface is only ever used by JS components, and never implemented or called directly by C++ components, then you don't need to do all of the work with defining ICmpExample. Simply create a file binaries/data/mods/public/simulation/components/interfaces/Example.js:

Engine.RegisterInterface("Example");

You can then use IID_Example in JS components.

(There's no strict requirement to have a single .js file per interface definition, it's just a convention that allows mods to easily extend the game with new interfaces.)

Defining a new message type in C++

Think of a name. We'll use Example again. (The name should typically be a present-tense verb, possibly with a prefix to make its meaning clearer: "Update", "TurnStart", "RenderSubmit", etc).

Add to TypeList.h:

MESSAGE(Example)

Add to MessageTypes.h:

class CMessageExample : public CMessage
{
public:
CMessageExample(int x, int y) :
x(x), y(y)
{
}
int x;
int y;
};

containing the data fields that are associated with the message. (In some cases there may be no fields.)

(If there are too many message types, MessageTypes.h could be split into multiple files with better organisation. But for now everything is put in there.)

Now you have to add C++/JS conversions into MessageTypeConversions.cpp, so scripts can send and receive messages:

JS::Value CMessageExample::ToJSVal(const ScriptInterface& scriptInterface) const
{
return JS::ObjectValue(*obj);
}
CMessage* CMessageExample::FromJSVal(const ScriptInterface& scriptInterface, JS::HandleValue val)
{
return new CMessageExample(x, y);
}

(You can use the JS API directly in here, but these macros simplify the common case of a single object with a set of scalar fields.)

If you don't want to support scripts sending/receiving the message, you can implement stub functions instead:

{
return JS::UndefinedValue();
}
CMessage* CMessageExample::FromJSVal(const ScriptInterface& UNUSED(scriptInterface), JS::HandleValue UNUSED(val))
{
return NULL;
}

Defining a new message type in JS

If a message will only be sent and received by JS components, it can be defined purely in JS. For example, add to the file interfaces/Example.js:

// Message of the form { "foo": 1, "bar": "baz" }
// sent whenever the example component wants to demonstrate the message feature.
Engine.RegisterMessageType("Example");

Note that the only specification of the structure of the message is in comments - there is no need to tell the engine what properties it will have.

This message type can then be used from JS exactly like the CMessageExample defined in C++.

Component communication

Message passing

For one-to-many communication, you can send indirect messages to components.

From C++, use CComponentManager::PostMessage to send a message to a specific entity, and CComponentManager::BroadcastMessage to send to all entities. (In all cases, messages will only be received by components that subscribed to the corresponding message type).

CMessageExample msg(10, 20);
GetSimContext().GetComponentManager().PostMessage(ent, msg);
GetSimContext().GetComponentManager().BroadcastMessage(msg);

From JS, use Engine.PostMessage and Engine.BroadcastMessage, using the MT_* constants to identify the message type:

Engine.PostMessage(ent, MT_Example, { x: 10, y: 20 });
Engine.BroadcastMessage(MT_Example, { x: 10, y: 20 });

Messages will be received and processed synchronously, before the PostMessage/BroadcastMessage calls return.

Retrieving interfaces

You can also directly retrieve the component implementing a given interface for a given entity, to call methods on it directly.

In C++, use CmpPtr (see its class documentation for details):

...
CmpPtr<ICmpPosition> cmpPosition(context, ent);
if (!cmpPosition)
// do something to avoid dereferencing null pointers
cmpPosition->MoveTo(x, y);

In JS, use Engine.QueryInterface:

var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.MoveTo(x, y);

(The use of cmpPosition in JS will throw an exception if it's null, so there's no need for explicit checks unless you expect the component may legitimately not exist and you want to handle it gracefully.)

Testing components

Tests are critical for ensuring and maintaining code quality, so all non-trivial components should have test cases. The first part is testing each component in isolation, to check the following aspects:

To focus on these, the communication and interaction with other components is explicitly not tested here (though it should be tested elsewhere). The code for the tested component is loaded, but all other components are replaced with mock objects that implement the expected interfaces but with dummy implementations (ignoring calls, returning constants, etc). The details differ depending on what language the component is written in:

Testing C++ components

Create the file simulation2/components/tests/test_Example.h, and copy it from something like test_CommandQueue.h. In particular, you need the setUp and tearDown functions to initialise CXeromyces, and you should use ComponentTestHelper to set up the test environment and construct the component for you. Then just use the component, and use CxxTest's TS_* macros to check things, and use ComponentTestHelper::Roundtrip to test serialization roundtripping.

Define mock component objects similarly to MockTerrain. Put it in ComponentTest.h if it's usable by many component tests, or in the test_*.h file if it's specific to one test. Instantiate a mock object on the stack, and use ComponentTestHelper::AddMock to make it accessible by QueryInterface.

Testing JS components

Create the file binaries/data/mods/public/simulation/components/tests/test_ExampleTwo.js, and write

Engine.LoadComponentScript("ExampleTwo.js");
var cmp = ConstructComponent(1, "ExampleTwo");

where ExampleTwo.js is the component script to test, 1 is the entity ID, "ExampleTwo" is the component name. Then call methods on cmp to test it, using the TS_* functions defined in binaries/data/tests/test_setup.js for common assertions.

Create mock objects like

AddMock(1, IID_Position, {
GetPosition: function() {
return {x:1, y:2, z:3};
},
});

giving the entity ID, interface ID, and an object that emulates as much of the interface as is needed for the test.