14. Data Definition Language (DDL) for Duplicated Objects

The NetZ data definition language (DDL) enables developers to describe the data that needs to be sent over the network in a few simple statements, and then to generate the networking layer. That is, the developer uses one or more DDL files to describe the data structures and game objects for which NetZ manages the networking layer.

For a single game, the DDL compiler expects one or more DDL files. This chapter explains the DDL syntax, how to use the DDL compiler, and the class generation that is performed by the compiler. In addition, it describes in detail the information that you are required to provide.

14.1. DDL File Syntax

The NetZ DDL is an object-oriented language based on classes. The three most important classes are the DuplicatedObject, DupSpace, and DataSet classes. These classes are the base classes for all game objects and their related datasets. RMCs and actions are the two ways to remotely call a method. All duplicated objects, datasets, RMCs, and actions are declared in the DDL file. Comments within the DDL code are indicated by two backslashes at the beginning of the line.

14.1.1. Data Types

NetZ has several commonly used built-in data types, in addition to a number of supplementary data types that can be included if desired. The developer can also define custom data types. Once implemented, custom types can be used within the DDL like any other data type.

14.1.1.1. Built-In Types

Variables declared in the DDL may use any of the built-in data types, or any valid user-defined data type. The supported built-in data types are listed in the following table. An array of any of these types is also supported. For cstr, the variable is duplicated up to the terminating null character. A dataset sent over an unreliable channel cannot contain more than 3584 bytes of data. The DDL types are mapped to C++ types according to the following table.

Table 14.1 Mapping Built-In DDL Types to C++
DDL C++
bool qBool
byte qByte
int8 qInt8
int16 qInt16
uint16 qUnsignedInt16
int32 qInt32
uint32 qUnsignedInt32
int64 qInt64
uint64 qUnsignedInt64
float qFloat
double  qDouble
real qReal
qchar qChar
qchar8 qChar8
qchar16 qChar16
cstr qChar*
string String
dohandle DOHandle
stationurl StationURL
datetime DateTime
qresult qResult
qbuffer qBuffer
qlist qList
qvector qVector
qqueue qQueue
memberlist MemberList
memberqueue MemberQueue
membervector MemberVector

14.1.1.2. Supplied Extensions

To facilitate data manipulation, NetZ provides several additional data types that you can include when you compile your project. In particular, you can use the following C++ STL data types.

Table 14.2 Mapping Optional DDL Types to C++
DDL C++
std_string qChar
std_char8string qChar8*
std_char16string qChar16*
std_bitset8 std::bitset<8>
std_bitset16 std::bitset<16>
std_bitset32 std::bitset<32>
std_vector T std::vector T
std_list T std::list T
std_map Key, Value std::map Key, Value

To integrate these STL data types into a project, you must use the following code to include the STL.ddl file in the DDL file of your project.

Code 14.1 Including the STL.dll File in a DDL File

#include <Extensions/STL.ddl>

Next, specify which of the following STL libraries to link with.

Table 14.3 STLExt Libraries Corresponding to Project Settings
Project Settings STLExt Library
Unicode debug STLExtud.lib
Unicode release STLExtu.lib

14.1.1.3. Custom Data Types

The developer can define custom data types in the DDL. After these data types are defined in the DDL, they can be used as if they were built-in types. The following syntax is used to define a custom type in the DDL.

Code 14.2 Defining a Custom Type

type TypeName;

TypeName is the name of the new type. When a new type is declared in the DDL, the developer must implement a C++ class with the name DDLTYPETypeName to implement marshalling (that is, C++ mapping and in/out streaming) for this new data type. Within this class, the type definition for C++ mapping must have the form typedef TypeName CPPType. For example, to integrate STL strings as DDL types, you could define the following type.

Code 14.3 Sample Custom Type Definition (DDL)

type std_string;

When this declaration is found in the MyDDL.ddl file, the MyDDLTypes.h file must contain a C++ class to marshall std_strings.

Code 14.4 Sample Custom Type Definition (C++ Class)

class DDLTYPE(std_string) {
    public:
        // mapping of DDL type to a C++ type
        typedef std::string CPPType;

        static void Add(Message* pMessageToSend,
                        const std::string& oString) {
            // Code to add the string to the message.
        }
        static void Extract(Message* pReceivedMessage,
                            std::basic_string<C>* pString) {
            // Code to extract the string from the message.
        }
};

You could also integrate a user-defined template as a DDL type by defining the following type.

Code 14.5 Integrating a User-Defined Template (DDL)

type umap <T1, T2, ... >;

Next, use the following code to implement the template. Note that a parameter specified in a template is a DDLType and not a CPPType.

Code 14.6 Integrating a User-Defined Template (C++)

template <class DDLKey, class DDLValue>
class DDLTYPE(umap) : public DDLType {
    public:
        typedef NS_QSTL Map<DDLKey::CPPType,
                DDLValue::CPPType> CPPType;

        static void Add(Message * pMessageToSend,
                        const CPPType &uMap){
            (*pMessageToSend) << (qUnsignedInt32)uMap.size();
            CPPType::const_iterator i;
            CPPType::const_iterator end = uMap.end();
            for( i = uMap.begin(); i != end; i++ ){
                DDLKey::Add( pMessageToSend, (*i).first );
                DDLValue::Add( pMessageToSend, (*i).second );
            }
        };
        static void Extract(Message *pReceivedMessage,
                            CPPType *pUMap ){
            qUnsignedInt32 uiSize;
            (*pReceivedMessage) >> uiSize;
            qUnsignedInt32 i;
            for(i = 0; i < uiSize; i++){
                DDLKey::CPPType k;
                DDLValue::CPPType v;
                DDLKey::Extract( pReceivedMessage, &k );
                DDLValue::Extract( pReceivedMessage, &v );
                (*pUMap)[k] = v;
            }
        };
}

The distribution includes complete implementations of these examples of custom data types. These examples can be found in files with the .stl extension, located in the include/Extensions/STL directory. In the STL.ddl and STLTypes.h files, the .cpp files define and implement standard C++ bitsets and strings.

If a custom data type is assigned to a variable, the developer must implement streaming to and from a message so that the system knows how to treat the data. When this data is sent between stations, the system creates a message, and then the developer must use the methods of the Message class to add and extract the data. Data can be added to the message by using the stream operators or the AddData and AddDataString methods. These methods respectively add data of a specific type or a string. Similarly, use the ExtractData method to extract data from the message buffer. Call GetPayloadSize to get the size of the message content. Call the GetLastError method to get the last error generated by the class. This method is particularly useful in determining whether an error occurred during a stream operation. The distribution includes a sample implementation of custom data types. These sample data types can be found in files with the STL extension, located in the ../include/Extensions/STL directory.

14.1.1.4. Required Member Functions

When implementing a custom type, you must implement the following methods in your user-implemented C++ class.

  • The default constructor.
  • A copy constructor. (Required only under some conditions.)
  • Assignment operator: operator=
  • Equality operator: operator==

The copy constructor is required only if one of the following conditions is met.

  • If you use the following custom type as the return type in an RMC: Custom foo()
  • If you pass the custom type by value in an RMC: Example: void foo(Custom toto)

If you only ever pass the custom type by reference, the copy constructor is not required.

14.1.1.5. Class Data Types

As an alternative to defining custom data types, data types can be declared as a class. This can simplify implementation in some cases. Class types are defined using the following syntax.

Code 14.7 Defining a Class Data Type

class ClassName [: inheritance_class] {
    class attributes
} [concrete];

in this example, class attributes can be any number of data types, as described previously, and inheritance_class refers to the name of another class from which ClassName inherits. From this declaration, the DDL compiler generates the ClassNameDDL.h/.ccp files and the DDLCLASS(ClassName) class, from which the ClassName class inherits. If you want to add functionality to the ClassName class, you must implement the class yourself. If not, you can use the concrete property. When this property is declared, the DDL compiler generates the base C++ class for the declared class. This class contains no methods and cannot be modified. For example, say that you declare the following in the DDL.

Code 14.8 Using the concrete Property

class Request{
    int32 m_intPlayerID;
    byte m_byType;
} concrete;

This generates the DDLCLASS(Request) class, and the RequestDDL.h and RequestDDL.cpp files. When the concrete property is specified, the Request class is generated within the RequestDDL.h/.cpp files and cannot be modified. When not using the concrete property, you are expected to implement the Request.h/.cpp files.

14.1.2. DDL Properties

Next, consider the built-in DDL properties of NetZ. The majority of these properties are only valid for dataset declarations. The only incompatible properties are buffered and extrapolation_filter, which cannot be declared together. You may also implement your own custom DDL properties as detailed later in this section.

DDL properties are primarily used to specify the manner in which a dataset is updated. The default assumption is that the variables of a dataset change so that each duplica is updated each time the DuplicatedObject::Update function is called, and that the data is sent over a reliable channel to guarantee delivery. If you want to update a dataset in a way that differs from these assumptions, you must include one or more DDL properties in the DDL declaration of the dataset.

Table 14.4 DDL Properties
Property Valid In Description
unreliable Dataset Dataset updates are sent over an unreliable channel. Updates are transmitted faster than over a reliable channel, but delivery is not guaranteed. This property is suitable for datasets that contain variables that change frequently, such as position, where a missing update is not detrimental to the game. A dataset sent over an unreliable channel cannot contain more than 3584 bytes of data. Normally, there is a possibility that data arrives that has duplicated data or for which the order has changed. With RootTransport::EnableDropDuplicateReorderingUnreliablePackets(true);, data that is duplicated or for which the order has been changed can be lost.
buffered Dataset The values of the dataset data members on the duplica are not directly updated. When an update message is received by the duplica, it stores the result in a buffer. The buffer content is copied to the dataset member variables when the application calls DuplicatedObject::Refresh. The buffer is automatically locked when data is transferred to or from it. This property cannot be used together with the extrapolation_filter property.
Constant Dataset The values of the dataset data members are constant. When the duplicated object is created, the dataset values are sent to its duplicas and—because they do not change—they are never updated.
update_perf_counter Dataset A DataSetPerfCounters class, accessed using DataSet::GetPerformanceCounters, is created for the dataset. Performance data for the dataset can be recorded to aid in fine-tuning and optimizing network performance. For more information, see Section 17.1 Performance Fine-Tuning.
upon_request_filter Dataset Dataset updates are sent only when DataSet::RequestUpdate is called. This property is used for variables that do not change often, such as weapons inventories.
station_filter Dataset This property is used to filter out the stations that a dataset is updated on, for optimization and security reasons. To specify this filter, you must implement the DataSet::UpdateIsRequired system callback to specify which stations to update the dataset on.
extrapolation_filter Dataset This property applies a prediction algorithm to the values of the dataset members on the duplicas. This property is suitable for datasets that change in a continuous way, such as position. NetZ also provides options to indicate that there can be occasional continuity breaks, even though the dataset usually changes in a continuous way. When using an extrapolation filter, you must call the DuplicatedObject::Refresh method to update the dataset members. A default value of zero is used when the error tolerance is not set. The valid types are int32, int16, int8, float, and double. This property cannot be used together with the buffered property.
Loopback Dataset This property indicates that the values of the dataset members on the duplication master are interpolated. This property can only be used with the extrapolation_filter property. The valid types are int, float, and double.
Concrete Dataset, class If this property is declared, the DDL compiler generates the base C++ class (the source and header files) for the declared dataset, which contains no methods and cannot be modified. Use this property when you do not wish to add any functionality to the dataset class. When adding functionality, do not specify this property. In this case, you must then implement the class yourself.
Globaldo Doclass All NetZ duplicated object classes are automatically instantiated as global duplicated objects. This means that they are known globally across the session and are guaranteed to be duplicated to all stations participating in the session.
nonglobal Doclass This property is not normally used. (These objects are global by default. To make them non-global, use the nonglobal property in the class declaration in the DDL file.)

14.1.3. Custom DDL Properties

If the DDL properties provided with NetZ are not adequate for your needs, you can implement your own custom dataset or duplicated object properties as described in this section. Custom dataset properties enable you to update your datasets in precisely the manner you choose, and custom duplicated object properties enable you to add custom code in the DDL-generated files.

14.1.3.1. Datasets

To implement your own dataset DDL properties, you must perform the following steps.

  1. Declare a custom dataset DDL property in your DDL file.
  2. Define the macros used by your dsproperty DDL declaration.
1. Declaring a Custom Dataset DDL Property

A custom dataset DDL property must be declared in the DDL file using the following syntax.

Code 14.9 Declaring a Custom Dataset DDL Property

dsproperty PropertyName : <property qualifier>;

PropertyName is the name of your custom DDL property. property qualifier is one or more qualifiers that define the type of DDL property. The specified property qualifiers can be updatefilter or code. After you have declared your custom dataset DDL property, you can then assign it to the appropriate datasets in the usual manner.

Property Qualifiers

You can specify the following property qualifiers in a doclassproperty declaration.

Code 14.10 Property Identifiers

code

This property qualifier enables you to add custom code in the file generated by the DDL compiler for a particular duplicated object. The following four macros are used by the generated code.

Code 14.11 Macros Used by Code Generated for Property Identifiers

// public declaration
_PR_PropertyName_pudecl()

// private declaration
_PR_PropertyName_prdecl()

// source file implementation
_PR_PropertyName_impl(DOCLASS)

// source file implementation of a derived class
_PR_PropertyName_derived_impl(DOCLASS,PARENT)

PropertyName is the name of your custom duplicated object DDL property. DOCLASS is the name of the DuplicatedObject class that uses the property. PARENT is the name of the DuplicatedObject class from which the DOCLASS inherits.

For example, in the DDL file, define the MyDO class to use a property that specifies the code property qualifier. The previous four macros are then used by the DDL-generated DATASET(MyDO) class. More specifically, the first two macros are expanded respectively in the public and private declarations of the MyDODDL.h file, and the last two macros are expanded in the MyDODDL.cpp file.

2. Defining Macros
After making the relevant DDL declarations, you must then define the appropriate macros to use in the generated code, in the DDLFileNameProperties.h file. By using the macros, you define the name of the class that implements the associated DDL property.

14.1.3.2. Duplicated Objects

To implement your own duplicated object DDL properties, you must do the following.

  1. Declare a custom duplicated object DDL property in your DDL file.
  2. Define the macros used by your doclassproperty DDL declaration.
1. Declaring a Custom Duplicated Object DDL Property

You must declare custom duplicated object DDL properties in the DDL file using the following syntax.

Code 14.12 Declaring Custom Duplicated Object DDL Properties

doclassproperty PropertyName : <property qualifier>;

PropertyName is the name of your custom DDL property. property qualifier is one or more qualifiers that define the type of DDL property. The specified property qualifier can only be code.

After declaring a custom duplicated object DDL property, you can assign it to the appropriate duplicated objects in the usual manner.

Property Qualifiers

You can specify the following property qualifiers in a doclassproperty declaration.

Code 14.13 Property Identifiers

code

This property qualifier enables you to add custom code in the file generated by the DDL compiler for a particular duplicated object. The following four macros are used by the generated code.

Code 14.14 Macros Used by Code Generated for Property Identifiers

// public declaration
_PR_PropertyName_pudecl()

// private declaration
_PR_PropertyName_prdecl()

// source file implementation
_PR_PropertyName_impl(DOCLASS)

// source file implementation of a derived class
_PR_PropertyName_derived_impl(DOCLASS,PARENT)

PropertyName is the name of your custom duplicated object DDL property. DOCLASS is the name of the DuplicatedObject class that uses the property. PARENT is the name of the DuplicatedObject class from which the DOCLASS inherits.

For example, in the DDL file, define the MyDO class to use a property that specifies the code property qualifier. The previous four macros are then used by the DDL-generated DATASET(MyDO) class. More specifically, the first two macros are expanded respectively in the public and private declarations of the MyDODDL.h file, and the last two macros are expanded in the MyDODDL.cpp file.

2. Defining Macros
After making the relevant DDL declarations, you must then define the appropriate macros to use in the generated code, in the DDLFileNameProperties.h file. By using the macros, you define the name of the class that implements the associated DDL property.

14.1.4. Application Titles

The application title defines the type of game. It is necessary to define an application title in the DDL file because it is used to authenticate any station that tries to join the session. If more than one DDL file is used, the application title must be defined in a DDL file that includes or uses all the other relevant DDL files. The application title is also included in the SessionDescription. It should not be confused with the session name, which is used to identify a specific instance of a type of game. The session name is defined when a session is created using the Session::CreateSession member function, as detailed in Section 5.1 Session Management. Unlike the application title, the session name does not have to be defined. The application title is defined in the DDL using the following syntax.

Code 14.15 Defining the Application Title

title "string";

To define the application title of a game as SphereZ, you would make the following declaration in the DDL file.

Code 14.16 Example of Defining the Application Title

title "SphereZ";

14.1.5. Duplicated Object Classes

In a multiplayer game, the objects of the session must be duplicated across the network for the players to see each other. NetZ uses object duplication as the mechanism to achieve this. NetZ duplicated objects are declared using duplicated object classes (DOClasses). A duplicated object is an instance of a duplicated object class, and you can create multiple instances of the same duplicated object class.

The relationship between a duplicated object and a duplicated object class is very similar to the relationship between an object and a class in standard C++ programming. The distinction between C++ objects and NetZ duplicated objects is apparent when an object is instantiated. In NetZ, whenever an instance of a duplicated object class is created on a particular station, copies of the instance are automatically created on all remote stations that require that object. The local duplicated object is called the duplication master and is the controlling instance of the object. The other duplicated objects are its duplicas.

A duplicated object class is declared in a DDL file, which also declares the relevant datasets, RMCs, and actions. The other elements of the DDL file are explained in more detail later.

Use the following syntax to declare a duplicated object in the DDL file.

Code 14.17 Declaring a Duplicated Object

doclass ObjectName [: inheritance_class] {
    [declarations];
};

[inheritance_class] refers to the name of another duplicated object class from which the ObjectName class inherits. [declarations] are all of the relevant dataset, RMC, and action declarations for the class.

By default, all objects in NetZ are global. Each class defined in the DDL is known globally across the session, and is guaranteed to be duplicated to all stations participating in the session. (You do not normally need to make these classes non-global.)

Duplicated objects can also be WellKnown objects. The difference between a global duplicated object and a WellKnownDO is that a WellKnown object is created before the session. When a station joins a session, the station is guaranteed to discover all WellKnown objects before the Session::JoinSession member function returns on the station. WellKnown objects are also fault-tolerant by default, and migrate to a new station if their duplication master resides on a station that fails or leaves the session.

WellKnown instances of a particular duplicated object class are declared in the DDL file. When a duplicated object is declared as a WellKnown object, the DDL compiler declares and initializes a global variable of type WKHandle on each station. This variable can be used to construct a DOHandle to the object. You must use the DuplicatedObject::CreateWellKnown function to create these instances on the session master, after which they are duplicated to all stations that belong to the session.

Code 14.18 Declaring a WellKnown Instance

wellknown DOClassName aVariableName;

In our SphereZ example, we create a WellKnown instance of the DOClass World as follows.

Code 14.19 Example of Declaring a WellKnown Instance

wellknown World g_hWorld;

One instance of the World is well-known, so global variable g_hTheWorld of type WKHandle is defined in the file generated by the DDL compiler. Similarly, the compiler generates a variable of type DOHandle for each class defined in the DDL. This variable is used to refer to a duplicated object of a particular class. The WKHandle instance inherits from the DOHandle class. For more information about implementing the WellKnownDO class, see Section 11.4 WellKnown Objects.

Part of the DDL file for the hypothetical SphereZ sample program is shown in the following example. Four duplicated object classes are declared in the DDL: GeometricalObject, Sphere, AISphere, and World. Each of these DOClasses can contain datasets, RMCs, and actions. The AISphere datasets, RMCs, and actions are inherited from the Sphere class. The datasets, RMCs, and actions of the Sphere class are in turn inherited from the GeometricalObject class. This example is expanded upon elsewhere in this manual to introduce new concepts.

Code 14.20 Partial DDL for the SphereZ Sample Program

doclass GeometricalObject {
    [declarations];
};

doclass Sphere : GeometricalObject {
    [declarations];
};

doclass AISphere : Sphere {
    [declarations];
};

doclass World {
    [declarations];
};

wellknown World g_hWorld;

You must write the source and header files and then implement the class for each duplicated object class that you define in the DDL file. For more information, see Section 14.5 Developer Responsibilities and Section 11.1 Creating Duplicated Objects.

14.1.6. Datasets

A dataset is defined in the DDL using the following syntax.

Code 14.21 Defining a Dataset

dataset DatasetName {
    Dataset attributes;
}[Update Policy];

Dataset attributes can be any number of simple data types described in Section 14.1 DDL File Syntax. However, when using an extrapolation filter, you can only use the types int, float, and double.[Update Policy] specifies the manner in which the dataset is updated by using any combination of valid DDL properties, as described in Section 14.1.

When more than one DDL property is specified, separate each property by using a comma.

The default assumption is that the variables of a dataset change so that each duplica is updated each time the DuplicatedObject::Update function is called, and that the data is sent over a reliable channel to guarantee delivery. If you want to update a dataset in a way that differs from these assumptions, you must include one or more DDL properties in the DDL declaration of the dataset.

Continuing the SphereZ example started previously, define three datasets in the DDL: Position, SphereData, and Weather. The following sections shows the update policies for these datasets.

14.1.6.1. Position

This dataset contains the variables x, y, and z, which define the location of a sphere. A prediction algorithm is used to update the Position dataset, and updates are sent over an unreliable channel. The extrapolation filter instructs NetZ to predict the values for x, y, and z between network updates, which smooths the dataset values.

14.1.6.2. SphereData

This dataset contains the Texture variable that defines the appearance of a sphere. The updates for the SphereData dataset are sent over a reliable channel, and are updated only when requested. This dataset does not change often, but it is important that the object is notified of every change.

14.1.6.3. World

This dataset contains the FogEnabled variable that defines whether fog is turned on or off. The World dataset updates are sent using the default update policy. This means that they are sent over a reliable channel each time the data changes.

Code 14.22 Example of Defining a Dataset

dataset Position {
    double x;
    double y;
    double z;
} unreliable, extrapolation_filter;

dataset SphereData {
    uint16 m_ui16Texture;
} upon_request_filter;

dataset World {
    int FogEnabled;
}

Any dataset defined in the DDL must be associated with a duplicated object. For example, use the following syntax to associate the Position dataset with the GeometricalObject duplicated object class.

Code 14.23 Example of Associating a Dataset With a Duplicated Object Class

doclass GeometricalObject {
    Position m_dsPos;
    // other declarations
};

Because the Sphere duplicated object class inherits from GeometricalObject, the Sphere class includes the Position dataset.

When declaring a dataset in the DDL file, the dataset must be implemented by the user, as detailed in Section 11.6 Datasets.

14.1.6.4. MemberContainers

The containers used in NetZ function in a very similar manner to the containers of the C++ standard library. For more information about the usage of containers, refer to any good C++ programming book. The kinds of containers used in NetZ are membervector, memberlist, and memberqueue, and the supported types for the containers are the same as those listed previously for dataset variables, except for the string type. When declared in the DDL, a container must specify that updates are sent over a reliable channel, which is the default, and they cannot specify the use of an extrapolation filter. The following gives a sample DDL definition of a MyContainer dataset class that contains three containers and is associated with the Avatar duplicated object class.

Code 14.24 Sample Container Definitions

dataset MyContainer{
    membervector<double> vec;
    memberlist<dohandle> lst;
    memberqueue<int32> que;
};
doclass Avatar{
    MyContainer m_dsContainer;
};

TYPE is any of the indicated DDL dsts types. Use the following code to create an iterator for this container.

Code 14.25 Container Iterator

MyContainer::vecIterator

14.1.7. Remote Method Calls

NetZ uses either remote method calls (RMCs) or actions to remotely call a method on a duplicated object. Remote method calls are described in this section, and actions are described in the following Section 14.1.8 Actions. RMCs are methods that are called on one specific instance of an object, which can be a duplication master or a duplica, and can also return a value. RMCs give the developer significantly more control over the method than actions both in how they are called and the information that the developer may get about the state and the outcome of the call.

Remote method calls are declared in the DDL file, and can contain parameters. The system assigns each declared RMC a unique MethodID. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1 DDL File Syntax, and any valid custom data type. When a built-in template type (MemberVector, MemberList, or MemberQueue) is defined as an RMC argument, NetZ expects that the user-defined function uses a parameter type of the following form.

Code 14.26 Available Parameter Types in User-Defined Functions

MemberVector <DDLTYPE(type)>,
MemberList <DDLTYPE(type)>,
or MemberQueue <DDLTYPE(type)>

An RMC uses the following syntax.

Code 14.27 Syntax Used for an RMC

returntype RMCName( [Argument List] );

returntype is either the parameter type of the return value. or void. [Argument List] can be any number of arguments, similar to a C++ function declaration.

The arguments of the input and output of the RMC can be specified using the in, out, or in out modifiers as shown in the following example. In this example, a CarDealer duplicated object class has the RMCs BestMatch, ListMatches, and UpdateList. The first two methods respectively return the vehicle that best matches the specified criteria, which consists of the vehicle make, model, year, and price, and a list of all the matches for the criteria. The UpdateList method updates the current list of available vehicles.

Code 14.28 RMC for the CarDealer Class

doclass CarDealer {
    dohandle BestMatch(int32 imake, int32 imodel, int32 iyear, int32 iprice);
    void ListMatches(int32 imake, int32 imodel, int32 iyear,
                     out int32 iNbOfCars, out dohandle hCarArray[16]);
    void UpdateList(in out dohandle hCarArray[16]);

    // other declarations
};

When declaring an RMC in the DDL file, it must be implemented by the user, as detailed in Section 13.1 Remote Method Calls.

14.1.8. Actions

NetZ uses either remote method calls (RMCs), as described earlier, or actions to remotely call methods on duplicated objects. Actions are methods that are called within a duplicated object class, and are the easiest way to call a method on an object to ensure that the method is executed on all of the duplicas of the object. However, as opposed to an RMC, an action cannot return a value.

Actions can be called on the duplication master or duplica of an object. When an action is called on a duplication master, the duplication master then calls the corresponding method on all of its duplicas. If an action is called on a duplica, that duplica calls the corresponding method on its duplication master. If necessary, the duplication master then updates the datasets of its duplicas.

Actions are declared in the DDL file and can contain parameters. The system assigns each declared action a unique MethodID. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1 DDL File Syntax, and any valid custom data type. When a built-in template type (MemberVector, MemberList, or MemberQueue) is defined as an action argument, NetZ expects the user-defined function to use a parameter type of the form MemberVector <DDLTYPE(type)>, MemberList <DDLTYPE(type)>, or MemberQueue <DDLTYPE(type)>.

The following syntax is used for actions. [Argument List] can be any number of arguments, similar to a C++ function declaration.

Code 14.29 Action Syntax

action ActionName( [Argument List] );

In SphereZ, the act of updating the weather is defined as an action because it is performed within a duplicated object class and can be called remotely. The following code shows declaration of the UpdateWeather action in the DDL file.

Code 14.30 UpdateWeather Action Declaration

doclass World {
    // other declarations
    action UpdateWeather();
};

When declaring an action in the DDL file, it must be implemented by the user, as detailed in Section 13.2 Actions.

14.1.9. Constants

If you want to specify constants that may be shared between the DDL and C++ code, you may use the #define directive. You must simply define the constant and its value, as shown in the following code sample. ConstantName is the name of the constant and Value is its value.

Code 14.31 Defining a Constant

#define ConstantName Value

For example, the hypothetical MyConstant constant could be defined as follows.

Code 14.32 Example of Defining a Constant

#define MyConstant 15

Note that for DDL files that specify constants, it is usually simpler to include the file using the #include DDL preprocessor directive instead of the #use directive, especially if all that is defined in the file is constants.

14.2. DDL Preprocessor Directives

DDL declarations may be made in one or more DDL files. If multiple DDL files are used, use the #include and #use preprocessor directives to ensure that all relevant information is available to the DDL compiler. Preprocessor directives are typically used to make source programs easy to change and easy to compile in different execution environments. The DDL preprocessor directives work the same way as the preprocessor directives in standard C++ programming.

The DDL preprocessor recognizes the following directives.

Table 14.5 NetZ DDL Preprocessor Directives
Property Description
#define macro  
#define macro string You can use the #define directive to give a meaningful name to a constant in your program. However, as opposed to standard C++ syntax, a macro cannot be defined to take arguments. The DDL compiler automatically defines the NETZ macro.
#else The #else directive can appear between the #ifdef / #ifndef and #endif directives. Only one #else directive is allowed, and if present, it must be the last directive before #endif.
#endif Each #ifdef and #ifndef directive in a source file must be matched by a closing #endif directive.
#ifdef identifier  
#ifndef identifier The #ifdef directive, with the #else and #endif directives, controls compilation of portions of a source file. This constant expression is considered true (nonzero) if the identifier is currently defined; otherwise, the condition is false (zero). The #ifndef directive checks for the opposite of the condition checked by #ifdef. If the identifier has not been defined, the condition is true (nonzero); otherwise, the condition is false (zero). Each #ifdef directive in a source file must be matched by a closing #endif directive. Only one #else directive is allowed, and if present, it must be the last directive before #endif.
#include filename The #include directive tells the preprocessor to treat the statements in the specified file as if they had appeared in the source program at the point where the directive appears. The preprocessor looks for the files in the local directory, searches in the path specified by the -I compiler option, and finally searches the Frameworks subdirectory of the installation directory.
#use filename The #use directive works in a similar manner to #include, except that the DDL compiler does not generate code for the classes and other elements in the DDL files. The preprocessor looks for files in the local directory, searches in the path specified by the -I compiler option, and finally searches the Frameworks subdirectory of the installation directory.

14.3. Using the DDL Compiler

When a project is compiled, the DDL compiler follows the following rules. These rules ensure that the compiler knows where the generated files are located.

  1. The DDL regenerates files only when their content has changed. This speeds up compilation by ensuring that files that haven’t changed are not recompiled.
  2. DDL-generated files are always generated in the same directory as their associated DDL file. The user-implemented files for definitions made in the DDL file must also be located in this directory.
  3. If a DDL file includes another DDL file, the generated files of the included DDL are generated in the directory of the file that includes it. In other words, the content of the included file is compiled as if it was part of the file that included it.
  4. If a DDL file uses another DDL file, the paths in the #include lines of the generated files are the same as the paths to the used files. For example, if #use path/MyFile.ddl is found in a DDL file, the compiler generates #include “path/MyFileDDF.h”.
  5. If a generated file must include another file from the local DDL, by default the path for the #include lines has no prefix. To specify a prefix, use the –includeprefix path compiler option.

The following syntax is used to invoke the DDL compiler and to generate the code to implement the objects and datasets of the session described in the DDL file.

DDL.EXE [options] filename

filename specifies the name of the DDL file. The options that can be used with this command are detailed in the following table.

Table 14.6 DDL Compiler Options
Option Description
-D macro Defines the specified macro.
-header (or -p) HeaderFilename Specifies the file to include at the beginning of all the generated .cpp files. This option is useful for projects with precompiled headers. For example, when you build a NetZ project that uses the precompiled header stdheaders.h, invoke the DDL compiler with the -header stdheaders.h switch.
-help (or -h) Displays the Help screen that details the DDL compiler options.
-I path Specifies a path to search in addition to the local directory when a DDL file is either included or used in another DDL file. In general, this path is also passed to the C++ compiler.
-includeprefix path Specifies a path to use as a prefix when the generated code must include a file. For example, when specifying ddl.exe –includeprefix Game MyFile.ddl, the compiler generates #include “Game/MyFileDDF.h”.
-nousingdirective The DDL compiler adds NO_USING_QUAZAL_DIRECTIVE at the beginning of each generated file. Use this option if you do not want to use the namespace by default.
-skeleton (or –g) The DDL compiler generates a skeleton of the required user files that you can use as a starting point to implement your duplicated object classes. Note that existing files are never overwritten.
-verbose (or –v) Verbose.

For the errors and warning codes returned by the DDL compiler, see Section 15.3 DDL Compiler Errors. For example, use the following syntax to compile the SphereZ.ddl DDL file.

Code 14.33 Sample Compilation

ddl.exe SphereZ.ddl

14.4. Compiler Output and Class Generation

When you compile an application built with NetZ, the NetZ DDL compiler uses the DDL files to generate the DOCLASS, DATASET, and DUPSPACE classes and their relevant source and header files. The compiler generates the source and header files ClassNameDDL.cpp and ClassNameDDL.h from the DDL files for each dataset and duplicated object declaration made in the DDL. The generated classes and files, along with the user-implemented classes and game code and the NetZ library, are then used by the C++ compiler to generate the executable file. The following figure illustrates this process.

_images/Fig_DO_DDL_Create_ExecutableFile.png

Figure 14.1 Generating an Executable File

14.4.1. Generating C++ Classes

The NetZ DDL compiler generates C++ classes corresponding to the description declared in the DDL file. The main user-implemented classes for NetZ are the duplicated object and dataset classes. These user-implemented classes inherit from the classes generated by the DDL compiler, which in turn inherit from the NetZ classes DuplicatedObject and DataSet. The following figure illustrates the class hierarchy.

_images/Fig_DO_DDL_ClassInheritance.png

Figure 14.2 General Class Inheritance for Duplicated Objects and Datasets

14.4.1.1. Implementing

In general, if you specify a duplicated object class UserDOClass in the DDL, you must implement a UserDOClass class that inherits from the DDL-generated class. Use DOCLASS(UserDOClass) to access this generated class. This DOCLASS is a macro that builds a name that refers to the generated class. Any class generated as a result of a DOClass declaration in the DDL file automatically ultimately inherits from the root class DuplicatedObject. Unlike the DataSet class, you can specify inheritance between user-implemented classes in the DDL for the DuplicatedObject class.

Similarly, if you specify a UserDataSet dataset in the DDL, you must implement a UserDataSet class that inherits from the DDL-generated class. Use DATASET(UserDataSet) to access this generated class. This DATASET is a macro that builds a name that refers to the generated class. The DDL-generated class automatically ultimately inherits from the root class DataSet. The variables declared as part of the UserDataSet class inherit from the variables declared in the generated class DATASET(UserDataSet).

14.4.1.2. Instantiation

You cannot instantiate duplicated objects or datasets directly. Use the DuplicatedObject::Create member function to create duplicated objects. This method creates an instance of the class that can be published. Datasets do not need to be directly instantiated. They are implemented automatically by NetZ according to the declarations in the DDL file. For dataset classes, the DDL-generated code implements the classes as member variables.

For duplicated objects, the class generated by the DDL compiler encapsulates all the network code. This deals with how to instantiate a class and ensure that it is discovered on all remote stations, how to ensure that duplicas are updated appropriately when the variables of a duplicated object change, and how to remotely call methods when an RMC or action is called. The user-implemented class declares the dataset variables, implements methods corresponding to the declared RMCs and actions, and may also add any local variables or methods pertinent to the application.

Similarly, for datasets the class that the DDL compiler generates holds the dataset variables, and is responsible for the dataset update, including marshalling, unmarshalling, and data streaming. If an extrapolation filter is used to update the dataset, a prediction model is also generated. The user-implemented class adds any local variables or methods that are applicable to the game. This local information makes no sense on a remote station and is therefore not replicated.

14.4.1.3. Understanding the SphereZ Sample

Previous sections defined the duplicated objects, datasets, and actions relevant for the SphereZ example. The following example shows a complete DDL file for SphereZ.

Code 14.34 SphereZ DDL File

title “SphereZ”

dataset Position {
    double x;
    double y;
    double z;
} extrapolation_filter, unreliable;

dataset SphereData {
    uint16 m_ui16Texture;
} upon_request_filter;

dataset Weather {
    int FogEnabled;
};

doclass GeometricalObject {
    Position m_dsPos;
};

doclass Sphere : GeometricalObject {
    SphereData m_dsSphereData;
};

doclass Sphere : AISphere {
};

doclass World {
    Weather m_dsWeather;
    action UpdateWeather();
};

wellknown World g_hWorld;

The class inheritance of duplicated objects and datasets—the result of the descriptions in this DDL file—is illustrated in Figure 14.3 and Figure 14.4. The AISphere user-defined duplicated object class inherits from the Sphere class, which inherits from the GeometricalObject class, which in turn inherits from the NetZ DuplicatedObject class. Similarly, the Position user-defined dataset class automatically inherits from the NetZ DataSet class.

_images/Fig_DO_DDL_DOClassInheritance.png

Figure 14.3 Duplicated Object Class Inheritance

_images/Fig_DO_DDL_DatasetInheritance.png

Figure 14.4 Dataset Class Inheritance

For each class that is defined in the DDL, the inheritance of the class is defined in the corresponding header file. In this example, AISphere.h defines the AISphere class as inheriting from DOCLASS(AISphere), which is generated based on the definition in the DDL file. The generated file has the name AISphereDDL.h. Similarly, the DDL generates the classes DOCLASS(Sphere) and DOCLASS(GeometricalObject), and the files SphereDDL.h and GeometricalObjectDDL.h. The GeometricalObject class then ultimately inherits from the NetZ DuplicatedObject class. The user must include the DDL-generated header files before the corresponding user class is defined. The compiler also generates a source file corresponding to each defined class. These files must be linked to the other classes and compiled with the project. In this example, the source files generated by the DDL are AISphere.cpp, SphereDDL.cpp, GeometricalObjectDDL.cpp, and WorldDDL.cpp.

The Position dataset is defined in the Position.h file. The Position dataset class inherits from the DDL-generated class DATASET(Position), which ultimately inherits from the NetZ DataSet class automatically. The variables declared as part of the Position class inherit from the variables declared in the DATASET(Position) generated class. The files generated by the DDL have the names PositionDDL.h and PositionDDL.cpp. As mentioned previously, the user must include the generated header files before the corresponding user classes are defined, and the source files must be compiled with the project.

When a duplicated object is instantiated, the member variable that corresponds to the DOClass declaration uses the developer's own class rather than the class generated by the DDL compiler. For example, the DOCLASS(GeometricalObject) generated by the DDL compiler includes a variable m_dsPos of type Position, rather than DATASET(Position). As a result, code added in the Position class is available when the duplicated object is instantiated. The following figure illustrates this situation.

_images/Fig_DO_DDL_DO_Dataset_Relation.png

Figure 14.5 Relationship Between Duplicated Object and Dataset Classes

14.4.1.4. Inheritance

For each class that is defined in the DDL, the inheritance of the class is defined in the corresponding header file. In this example, the Moderator.h file defines that the Moderator class inherits from DOCLASS(Moderator), generated based on the definition in the DDL file. The generated file has the name ModeratorDDL.h. Similarly, the DDL generates the classes DOCLASS(Delegate), DOCLASS(Sage), DOCLASS(Room), and DOCLASS(SageBalancing), and the files DelegateDDL.h, SageDDL.h, RoomDDL.h, and SageBalancing.h. The Moderator class then inherits from the Delegate class and ultimately from the NetZ DuplicatedObject class. The user must include the DDL generated header files before the corresponding user class is defined. The compiler also generates a source file corresponding to each defined class. These files must be linked to the other classes and compiled with the project. In this example, the source files generated by the DDL are DelegateDDL.cpp, ModeratorDDL.cpp, SageDDL.cpp, RoomDDL.cpp, and SageBalancingDDL.cpp.

The Location dataset is defined in Location.h. The class inherits from the DDL-generated class DATASET(Location), which automatically ultimately inherits from the NetZ DataSet class. The variables declared as part of the class inherit from the variables declared in the generated class DATASET(Location). The DDL-generated files have the names LocationDDL.h and LocationDDL.cpp. Likewise, the DDL generates the files StringInfoDDL.h, StringInfoDDL.cpp, SageKnowledgeDDL.h, and SageKnowledgeDDL.cpp that correspond to the StringInfo and SageKnowledge datasets. As always, the user must include the generated header files before the corresponding user classes are defined, and compile the source files with the project.

When a duplicated object is instantiated, the member variable that corresponds to the DOClass declaration uses the developer's own class rather than the class generated by the DDL compiler. For example, the DOCLASS(Location) generated by the DDL compiler includes a variable m_dsLocation of type Location, rather than DATASET(Location). As a result, code added in the Location class is available when the duplicated object is instantiated, as shown in the following figure.

_images/Fig_DO_DDL_DDL_DO_Dataset_Relation.png

Figure 14.6 Relationship Between the Duplicated Object and Dataset Classes

14.5. Developer Responsibilities

The developer must implement several files before an application built with NetZ can be compiled. It is the developer’s responsibility to write the DDL files in the manner described previously in this chapter. In addition, for each dataset, doclass, and dupspace declaration made in the DDL, you are required to implement the .h and .cpp files. Within the header file, you must define the inheritance and the variables of the class, in addition to any non-dataset variables that are used for local processing.

You also need to ensure that the files generated by the DDL compiler are correctly linked to the project. To do this, you must make sure that all DDL generated header files (ClassNameDDL.h) are included before the class is defined, and that all DDL generated source files (ClassNameDDL.cpp) are linked and compiled with the project.

You must implement any class that is declared in the DDL file. In other words, you must implement the methods associated with any datasets, remote method calls, or actions. Use the following syntax to declare duplicated object and dataset classes in the header file.

Code 14.35 Declaring a Duplicated Object and Dataset Class

class DuplicatedObjectName : public DOCLASS(DuplicatedObjectName) {
    // User specified RMCs, actions, and methods
};

class DataSetName : public DATASET(DataSetName) {
    // User specified methods
};

You also need to implement the methods associated with any datasets, RMCs, or actions. For example, use the following syntax to declare the duplicated object class GeometricalObject in the GeometricalObject header file.

Code 14.36 Declaring GeometricalObject

class GeometricalObject : public DOCLASS(GeometricalObject) {
    // User specified RMCs, actions, and methods
};

All NetZ system classes are defined in the NEX namespace. The system uses the NEX namespace by default when you include NetZ. If you do not want to use the NEX namespace, possibly to avoid name collisions, use the –nousingdirective DDL compiler option and define the NO_USING_QUAZAL_DIRECTIVE macro before including NetZ or passing it by the command line.

You have the option of overriding the NetZ system defaults by implementing the appropriate code in the source and header files. For example, you can decide whether objects are fault-tolerant, whether objects can migrate, or whether the implementation of dead reckoning can be customized.

14.6. STL Data Types

To facilitate data manipulation, NetZ has several additional data types that can be included when a project is compiled. In particular, implementation of the following C++ STL string and bitset data types is provided.

Table 14.7 Mapping Optional DDL Types to C++
DDL C++ Comment
string String Implemented internally by using qChar16* (a pointer to a string of 8-bit or 16-bit characters). NetZ converts such strings to UTF-8 when placing them in RMCs.
cstr qChar Mapped to qChar16. No conversion to UTF-8 takes place when placed in RMCs.
qchar8 qchar8  
qchar16 Qchar16  
std_char8string qChar8* No conversion to UTF-8 takes place when placed in RMCs.
std_char16string qChar16* No conversion to UTF-8 takes place when placed in RMCs.
std_bitset8 std::bitset<8>  
std_bitset16 std::bitset<16>  
std_bitset32 std::bitset<32>  
qlist T qList T T is the type (such as int).
qvector T qVector T T is the type (such as int).
qmap Key, Value qMap Key, Value Key, Value are the key and value of the map items.
qqueue T qQueue T T is the type (such as int).
std_vector T std::vector T T is the type (such as int).
std_list T std::list T T is the type (such as int).
std_map Key, Value std::map Key, Value Key, Value are the key and value of the map items.

To integrate these STL data types into a project, you must use the following code to include the STL.ddl file in the DDL file of your project.

Code 14.37 Integrating STL Data Types Into a Project

#include <Extensions/STL.ddl>

CONFIDENTIAL