13. Extended Duplicated Object Features

This chapter describes extended features for duplicated objects. We recommend against using the extended features for duplicated objects until you have a solid understanding of their specifications.

13.1. Remote Method Calls

NetZ uses either Remote Method Calls (RMC) or actions, described in Section 13.1, to remotely call methods on duplicated objects. RMCs are methods that are called on one specific instance of an object, which may be a duplication master or a duplica, and they may return a value. RMCs give the developer significantly more control over the method than actions with respect to the manner in which they are called and the information the developer may obtain about the State and the Outcome of the call. An RMC may be performed both locally and remotely with respect to the station where it is called. Because an RMC is implemented as a C++ method, it may also be called directly on the local station. Up to 255 RMCs can be defined within a class.

RMCs are declared in the DDL, as described in Section 14.1 DDL File Syntax, and are implemented using the following syntax. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1, and any valid custom data type. When a built-in template type (that is, MemberVector, MemberList, or MemberQueue) is defined as an RMC 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)>.

Code 13.1 Remote Method Call Declaration

returntype RMCName( [Argument List] );

For example, in the Car Dealer example in Section 14.1 DDL File Syntax, the RMCs are implemented as methods of the CarDealer duplicated object class as follows.

Code 13.2 RMC Declaration in the CarDealer Duplicated Object Class

class CarDealer : public DOCLASS(CarDealer)
{
    dohandle BestMatch(qInt imake, qInt imodel, qInt iyear, qInt iprice);
    void ListMatches(qInt imake, qInt imodel, qInt iyear,
                     qInt *piNbOfCars, dohandle *phCarArray);
    void UpdateList(dohandle *phCarArray);
};

When an RMC is declared in the DDL, NetZ assigns a unique MethodID to the method to make it easy to identify, and the NetZ DDL compiler generates a stub to perform the RMC. The stub is used to call the method on the duplicated object. The name of the stub is obtained by adding the prefix Call to the name of the RMC. The syntax of the RMC stub is provided in the following code example, where RMCParameters are the arguments specified in the RMC declaration. RMCParameters is a parameter specified in the RMC declaration. If the call is successful, the stub returns true. If there is an error, it returns false. If the RMC specifies a return type other than void, the stub contains an additional pointer (pResult) to the result. If you do not care about the return result, you can pass NULL to the pResult parameter. The mapping of the arguments between the RMC and the stub is shown in Table 13.1.

Code 13.3 Stub for Executing an RMC

// RMC invocation stub for a method that specifies the return type
bool CallRMCName(RMCContext* pDOCallContext, type* pResult, RMCParameters)
// RMC invocation stub for a void method
bool CallRMCName(RMCContext* pDOCallContext, RMCParameters)

In the previous example the generated DOCLASS(CarDealer) defines the following stub.

Code 13.4 Stub Defined by CarDealer

bool CallListMatches(RMCContext* pDOCallContext, qInt imake, qInt imodel,
                    qInt iyear, out qInt *piNbOfCars, out dohandle *phCarArray)
Table 13.1 Mapping of Argument Types for Remote Method Calls
RMC Argument Type Modifier RMC Stub Argument Type
Simple type in Simple type
Simple type out Pointer to a simple type
Simple type in out Pointer to a simple type
Array of simple types in Pointer to the first element of a sequence of simple types
Array of simple types out Pointer to the first element of a sequence of simple types
Array of simple types in out Pointer to the first element of a sequence of simple types
String in Pointer to the first element of a sequence of qChar*
String out Pointer to the first element of a sequence of qChar*
String in out Pointer to the first element of a sequence of qChar*

To invoke an RMC the context of the RMC must be defined by using the DOCallContext class. By using this class, you set the station or stations you want the method to be called on, and the appropriate flags. By setting the appropriate flags you can make the RMC a synchronous or asynchronous call, or a one-way call, for example. You can also get information about the state and the outcome of the call, and have the ability to cancel or reset a call. Call contexts, which are used to specify the context of an RMC and the migration of objects, are detailed in Section 11.9 Call Contexts. Before setting any flags, refer to the description of each available flag described in Section 11.9 for more information about which flags may or may not be set together. Regardless of whether an RMC returns a value, or is one way, the RMC returns a Boolean value that indicates whether message was sent to the target station. In addition, an RMC that does not have a return value is not implicitly considered one way and its corresponding call context is completed. This makes it possible to use the RetryOnTargetValidationFailure flag on an RMC that does not return a value.

You can set the target station or stations of an RMC in a variety of ways. If the RMC has a return value, the target station may be set by using the DOCallContext::SetTargetStation function or by using the DOCallContext::CallOnMaster or DOCallContext::CallOnLocalStation flags. Ideally, call an RMC with a return value on only one station at a time, to avoid the possibility of returning multiple values for the same return value. Nevertheless, if you want to call the RMC on several stations you can do so by iterating over the stations. However, you must handle the multiple return values yourself.

On the other hand, if you do not want the RMC to return a value, set the OneWayCall flag, and then set the target station or stations by using the DOCallContext::SetTargetStation function or using one of the DOCallContext::CallOnMaster, DOCallContext::CallOnLocalStation, DOCallContext::CallOnDuplicas, or DOCallContext::CallOnNeighbouringStations flags. These last two flags provide an easy and efficient manner in which to call a method on several stations at one time. DOCallContext::CallOnDuplicas calls the method on all of an object's duplicas. This flag can only be called by the object’s duplication master. DOCallContext::CallOnNeighbouringStations calls the method on all neighboring stations of the local station, which is all of the stations in the session that the local station knows. Because every station knows every other station in a NetZ session, the method is called on all of the stations in the session except for the local station. If the CallContext::OneWayCall flag is set, target stations can be specified by setting multiple flags, but if the CallContext::OneWayCall flag is not set, only a single target can be specified by using the DOCallContext::SetTargetStation function or using one of the DOCallContext::CallOnMaster or DOCallContext::CallOnLocalStation flags. In addition, the CallContext::OneWayCall flag may be set for an RMC that returns a value. However, the return value is ignored.

When a reliable channel is used and the use of multiple reliables has been initialized, you can use DOCallContext::SetSubStreamID to specify a SubStreamID to use during calling. The same substream is used for RMC replies. For the initialization settings for the substream, see Section 7.3.4.

To complete the example, the following code shows what could be part of the event loop for the duplicated object class CarDealer. In this case, set the RMCContext::TargetObjectMustBeMaster flag to ensure that the ListMatches RMC is only called on the CarDealer object's duplication master.

Code 13.5 Partial Event Loop for the Duplicated Object Class CarDealer

CarDealer::Ref refdealer(g_hCarDealer);
RMCContext oDOCallContext(refDealer->GetMasterStation(), true);
oDOCallContext.SetFlag(RMCContext::TargetObjectMustBeMaster);
DOHandle phCars[16];
qInt iNbCars;

qBool bResult=refDealer->CallListMatches(&oDOCallContext, BMW, M5, 2001,
                                         &iNbCars, phCars);
    if (bResult){
        for(qInt i=0; i<iNbCars; i++)
        {
            printf("Found vehicle %x", phCars[i]);
        }
    }

The CallMethodOperation class ensures that embedded RMC calls do not deadlock while waiting for a return value. This is achieved by postponing the RMC call when it is not yet ready to return the result, and then recalling the RMC a short time later. In the meantime, any queued messages are processed so that if the original RMC relies on a queued message, that message is processed and the RMC call can be completed. In addition, this operation class may be used to control how, when, and by whom RMCs are called, and to return a DOHandle to the RMC callee station. All RMCs are uniquely identified by a MethodID that enables particular RMCs to be called or denied according to the station that calls them or the circumstances surrounding the call. For example, you may want to restrict certain RMCs so that they can only be called by a server station, during a game's setup, or at a specific point in the game. For more information about the CallMethodOperation class and examples of its use, see section 13.3.3 CallMethodOperation.

13.2. Actions

NetZ remotely calls methods on duplicated objects either by using Remote Method Calls, as described previously, or by using actions. Actions are the easiest way to call a method on all of a duplication master's duplicas, and to ensure that the method is executed on each station that has a copy of the duplicas and is participating in the session. An action is always performed within a duplicated object class, and may be performed both locally and remotely with respect to the station where the action is called. An action may either be called on a duplica, which then calls the method on its duplication master, or it may be called directly on a duplication master. Because an action is implemented as a C++ method, it may also be called directly on the local station.

RMCs are declared in the DDL, as described in Section 14.1.8 Actions. They are implemented using the following syntax. The valid arguments are the same as the valid arguments for datasets, as detailed in Section 14.1, and any valid custom data type. When a built-in template type (that is, 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)>.

Code 13.6 Action Declaration

action ActionName( [Argument List] );

For example, in SphereZ the UpdateWeather action is implemented as a method of the World duplicated object class, as shown in the following code.

Code 13.7 Implementation of the UpdateWeather Action in SphereZ

class World : public DOCLASS(World)
{
    // update the weather on the World object
    void UpdateWeather()
};

When an action is declared in the DDL, NetZ assigns a unique MethodID to the method that makes it easy to identify, and the NetZ DDL compiler generates two stubs to perform the action, although only one is used at a time. A stub is used to call the action on the duplication master or on the duplicas. The first stub calls the action on an object's duplication master, and may be invoked on any of the object's duplicas. The name of the stub is obtained by appending _OnMaster to the name of the action. In the previous example, the generated DOCLASS(World) defines the stub UpdateWeather_OnMaster(). The second stub calls the action on all of an object's duplicas, and may be invoked on the object's duplication master. The name of the stub is obtained by appending _OnDuplicas to the name of the action. In the previous example, the generated DOCLASS(World) defines the stub UpdateWeather_OnDuplicas(). The mapping of the arguments between the action and the stub are shown in the following table.

Table 13.2 Mapping of Argument Types for Actions
Action Argument Type Action Stub Argument Type
Simple type Simple type
Array of simple types Pointer to the first element of a sequence of simple types
String Pointer to the first element of a sequence of qChar*

To complete the example, the following code shows what would be part of the event loop for the World duplicated object class. The stub _OnDuplicas is used to update the weather on all duplicas, but the method is called directly on the duplication master to update the weather locally. In this example, the event loop for the duplication master triggers the action on all of its duplicas. As a result, every duplica calls the UpdateWeather method that is implemented in the World class.

Code 13.8 Partial Event Loop for the World Duplicated Object Class

Session::Ref refSession(Session::GetInstanceHandle());
if(refSession.IsValid() && refSession->IsADuplicationMaster())
{
    if (!DemoScene::GetInstance()->m_bEnableFog){
        World::Ref refWorld(g_hTheWorld);
        refWorld->EnableFog();
        refWorld->UpdateWeather_OnDuplicas();
    }
    else
    {
        World::Ref refWorld(g_hTheWorld);
        refWorld->DisableFog();
        refWorld->UpdateWeather_OnDuplicas();
    }
}

If an action is called on a duplication master at nearly the same time that the duplication master migrates, the action may either be called on a duplica or not at all if the duplication master has migrated because of a fault. As actions do not return a value, you are not notified when this occurs. If you must call a particular method on a duplication master, implement RMCs instead of actions. An RMC has the advantage of returning the outcome of the call, and also enables you to specify that the method must be called on a duplication master by using the DOCallContext::TargetObjectMustBeMaster flag.

13.3. System Operation

An operation is simply an event that occurs within the system, and is typically the result of a network message. The many operations that can occur in NetZ are classified into one of two types: duplicated object operations and session operations. Each of these types is further classified into several system-operation types. A duplicated object operation is performed on a particular duplicated object, while a session operation is performed on a particular session. For more information about the hierarchy of the operation classes, see Figure 13.1 Operation Class Hierarchy. The sequence of events that occurs during each operation is detailed later in this section. The OperationBegin and OperationEnd member functions of the DuplicatedObject and DataSet classes are called with a pointer to the operation class as a parameter.

_images/Fig_DO_Ext_OperationClass_Hierarchy.png

Figure 13.1 Operation Class Hierarchy

The member functions of the Operation class may be used to gain more information about a specific operation, and are particularly useful when implementing traces for debugging purposes. For more information about the implementation of system traces, see Section 17.2 Logging Data. To get the type of operation performed, you can use the GetType and GetClassNameString methods to return the operation type as a value or as a string respectively. You may attach a context to an operation by using the SetUserData and GetUserData member functions. The user-defined value passed with the UserContext class can be an unsigned long, double, Boolean, or pointer to an object. It is typically used to pass a value that you want to retain that is not automatically retained by NetZ. For example, to know when an object has migrated because of the occurrence of a ChangeMasterStationOperation operation, you can implement the following code.

Code 13.9 Detecting Object Migration Using a ChangeMasterStation Operation

void Avatar::OperationEnd(DOOperation* pOperation)
{
    switch (pOperation->GetType()) {
        case Operation::ChangeMasterStation:
            _printf(_T("The Avatar %x has migrated"),
                    pOperation->GetAssociatedDOHandle);
            break;
    }
}

The ChangeMasterStation and RemoveFromStore operations are not called on a duplicated object when the system state is locked by Scheduler::SystemLock. Locks can be used to ensure that an object does not migrate during a call, for example.

When system operations are traced, the Operation::SetTraceFilter member function determines which operations are traced and for which objects, allowing a callback to be registered. If this function is not redefined by the user, then by default all operations for all objects except CoreDO operations are traced. In other words, all UserDO operations are traced. If you want to filter the traced operations and objects, you must redefine the TraceFilter function and register a callback. This function takes a pointer to an operation and returns a Boolean value. After you have registered the callback, you must call the SetTraceFilter member function and set the TraceLog flag to trace operations. The following example shows how to call this method to trace selected operations on a particular user object class. For more information about the implementation of system traces, see section 17.2 Logging Data.

Code 13.10 Example of How to Trace Selected Operations on a User Object Class

// Register a callback that defines how the traces will be filtered
qBool FilterAvatarTraces(const Operation* pOperation)
{
    DuplicatedObject* pAssociatedDO =NULL;

    // Select the operations to be traced
    switch (pOperation->GetType())
    {
        case Operation::UpdateDataSet:
        case Operation::ChangeMasterStation:
        case Operation::FaultRecovery:
        case Operation::AddToStore:
        case Operation::RemoveFromStore:
        case Operation::CallMethod:
        case Operation::ChangeDupSet:
            pAssociatedDO =((DOOperation*) pOperation)->GetAssociatedDO();
            break;
        default:
            break;
    }
    if (pAssociatedDO ==NULL)
    {
        // Keep traces that are not related to a specific DO.
        return true;
    }

    // Trace the operation if the object is of the Avatar class.
    if (pAssociatedDO ->IsA(DOCLASSID(Avatar)))
    {
        return true;
    }
    else
    {
        return false;
    }
}

// To trace operations, call the SetTraceFilter method and
// then set the TraceLog flag to trace operations
Operation::SetTraceFilter(FilterAvatarTraces);
TraceLog::GetInstance()->SetFlag(TRACE_OPERATION);

NetZ defines an Operation::DynamicCast method for casting (explicit type conversion) during run time for each specific Operation class. This method functions in the same manner as the standard C++ dynamic_cast operator, but using NetZ run-time information. For example, you can use the following code to return a pointer to the RefreshOperation object if pOperation points to a RefreshOperation object.

Code 13.11 Example of Run-Time Casting

RefreshOperation * pRefreshoperation = RefreshOperation::DynamicCast(pOperation)

13.3.1. Operation Sequences

This section details the specific events that take place in a predefined sequence when a particular system operation occurs. It is important to understand the sequence of events for each operation so that you do not inadvertently deadlock your program. Note that between the calls to the OperationBegin and OperationEnd member functions of the DuplicatedObject and DataSet classes, NetZ uses SafetyExecutive to ensure that the block of code executed between these two member functions is only called in one thread and that no state changes occur during execution.

13.3.1.1. AddToStore

Each station maintains its own duplicated object store that contains a list of all the active local duplication master (locally published) and duplica (locally discovered) objects. Objects are added to and removed from the store as a result of objects being created and deleted at run time. Only objects in the store are considered active and updated on the local station. A duplication master is considered active if it exists on the local station. In other words, it is considered active if the DuplicatedObject::Publish function has been called and the DuplicatedObject::DeleteMainRef function has not yet been called. A duplica is considered active if its duplication master exists in the system.

The following sequence of events occurs when an object is added to the duplication master store.

When an object is added to the duplica store the following sequence of events occurs.

13.3.1.2. RemoveFromStore

Each station maintains its own duplicated object store that contains a list of all the active local duplication master (locally published) and duplica (locally discovered) objects. Objects are added to and removed from the store as a result of objects being created and deleted at run time. Only objects in the store are considered active and updated on the local station. A duplication master is considered active if it exists on the local station. In other words, it is considered active if the DuplicatedObject::Publish function has been called and the DuplicatedObject::DeleteMainRef function has not yet been called. A duplica is considered active if its duplication master exists in the system.

An object is removed from the duplication master store if the DuplicatedObject::ApproveFaultRecovery member function returns false or if the object is deleted by the DuplicatedObject::DeleteMainRef member function. The following sequence of events occurs when an object is removed from the duplication master store.

  • The SafetyExecutive function is called to determine whether the object may be deleted. If so, continue. If not, exit.
  • The DataSet::OperationBegin member function is called.
  • The DuplicatedObject::OperationBegin member function is called.
  • The duplication master object's duplicas are notified of the deletion by a multicast message.
  • The reference to the duplication master is deleted from the duplication master store.
  • The DataSet::OperationEnd member function is called.
  • The DuplicatedObject::OperationEnd member function is called. Iterations can no longer be performed over the duplication master object.
  • When the number of references to the object created using the RefTemplate class on the local station equals 0, the object's destructor is called.

An object is removed from the duplica store if the duplica receives a message from its duplication master notifying it that the master will be deleted. When an object is removed from the duplica store, the following sequence of events occurs.

13.3.1.3. ChangeDupSet

A ChangeDupSet operation is only called on a duplication master object. It is called each time the object creates or deletes a duplica.

When a duplica is added, the following sequence of events occurs.

  • The duplication master receives a message to create a duplica.
  • The SafetyExecutive function is called to determine whether the object may be created. If so, continue. If not, exit.
  • The DataSet::OperationBegin member function is called.
  • The DuplicatedObject::OperationBegin method is called on the duplication master station.
  • The master sends a message to the station where the duplica will be created to trigger an AddToStore operation on that station.
  • The duplica is added to the master's duplica location set.
  • The DataSet::OperationEnd member function is called.
  • The DuplicatedObject::OperationEnd member function is called.

When a duplica is removed, the following sequence of events occurs.

13.3.1.4. ChangeMasterStation

A ChangeMasterStation operation occurs as a result of the migration of a duplicated object's duplication master. As a result of this operation, the role of the object on the local station (master or duplica) may change. The object may either be promoted from a duplica to a duplication master, demoted from a duplication master to a duplica, or remain a duplica.

When a ChangeMasterStation operation is called, the following sequence of events occurs.

13.3.1.5. FaultRecovery

When a fault occurs in a station, there is a DO master in the station in which the fault occurred. With each DO session master,
the FaultRecovery operation is performed in the following order.

If the DuplicatedObject::ApproveFaultRecovery function returns true the session master takes control of the duplicated object with the following sequence of events.

If the DuplicatedObject::ApproveFaultRecovery function returns false, the object is removed from the store with the following sequence of events.

  • The RemoveFromStore operation is performed.
  • The DuplicatedObject::OperationEnd member function is called.
  • When the number of references to the object created using the RefTemplate class on the local station equals 0, the object's destructor is called.

13.3.1.6. CallMethodOperation

A CallMethodOperation occurs as a result of an RMC being called, when the following sequence of events occurs.

13.3.1.7. UpdateDataSet

When a duplica receives an update from its duplication master, this operation is called on the duplica and the following sequence of events occurs.

13.3.1.8. JoinSession

When a station attempts to join a session, the following sequence of events occurs.

13.3.1.9. JoinStation

When JoinSessionOperation::Approve is returned by the JoinApproval callback, the following next station join-in processes occur.

  • The station object of the joining station is published and JoinStationOperation starts.
  • The connection point station sends a message to the joining station confirming that the join request was approved.
  • The duplication master of the new station migrates from the connection point station to the joining station.
  • JoinStationOperation terminates.

13.3.1.10. LeaveStation

When a station leaves the session, the following sequence of events occurs.

13.3.2. Operation Security

NetZ system operations can be approved or denied before they are called. This prevents an operation from being called incorrectly. This task is performed by the SafetyExecutive class. An incorrect call may be caused by a bug in the game code or a hack performed by a player. Obviously, during internal or beta testing where cheating is unlikely, incorrectly called system operations are most probably caused by bugs in the game code.

A call to the SafetyExecutive::TrustLocalStation function sets whether a station is trusted or not. If the station is trusted, then the validity of any operations that the station triggers are not checked by the SafetyExecutive function before the operation is called. If the station is not trusted, then all operations are validated by the SafetyExecutive function before being called.

Restrictions on particular operations are implemented by using RegisterCustomOperationCheck. This member function takes as a parameter a user-defined CustomOperationCheck function, which dictates the conditions under which an operation is accepted or denied. This function is used to prevent an operation from being called incorrectly, and returns true if the operation is approved and false if it is denied. The action taken, if any, when an operation is denied is defined by the InvalidOperationCallback registered by RegisterInvalidOperationCallback. For example, you could use the following code to prevent Avatar objects from being created on a session client.

Code 13.12 Restrictions to Prevent Avatar Objects From Being Created on a Session Client

// Define the CustomOperationCheck
qBool CustomOperationCheck(const Operation & oOperation)
{
    // I will not create duplicas of my Avatar on a client station.
    if (oOperation.GetType()==Operation::ChangeDupSet)
    {
        const ChangeDupSetOperation *
                    pOp=ChangeDupSetOperation::DynamicCast(&oOperation);
        if (pOp->IsADuplicaAddition() && pOp->GetAssociatedDO()->
                                                       IsA(DOCLASSID (Avatar)))
        {
            Station::Ref refStation(pOp->GetTargetStation());
            if (refStation->GetProcessType() == Station::ClientProcess)
            {
                return false;
            }
        }
    }
    return true;
}

// Then register the custom check
SafetyExecutive::GetInstance()->
            RegisterCustomOperationCheck(DenyClientAvatarCreation);

When the CustomOperationCheck function returns false and the operation is denied by the SafetyExecutive function, NetZ calls the user-defined InvalidOperationCallback to determine what action to take. It is registered by using the RegisterInvalidOperationCallback function. This callback can define actions such as flagging the player or even kicking a player out of the game, if warranted. The following example shows how to implement a reporting system. This example assumes that the invalid operation call in debug or testing mode is caused by a bug, and outputs a trace when it occurs. On the other hand, in release mode the example assumes that an invalid operation might be caused by a cheat, and reports the event to the server that stores statistics for each station. If a station is deemed to be cheating, you can then take appropriate action.

Code 13.13 Implementation of a Reporting System for Invalid Operations

// Define the pfInvalidOperationCallback
void ReportCheats(const Operation& oOperation)
{
    // For example, at this point we could report to the server
    // that the player on station 'oOperation.GetOrigin()' has
    // performed an invalid operation and is potentially cheating.
    // This event would be reported via an RMC on a well-known
    // 'CheatManager' object. The server could then keep stats on
    // the reports and if a large number of people report that the
    // same person is cheating, appropriate action be taken. Note
    // that an invalid operation may also be an indication of a bug
    // in the game and not just a cheat.
}

void TraceInvalidOperations(const Operation& oOperation)
{
    // During development testing, we know that no one cheats. If
    // an invalid operation is called, then it is due to a bug in
    // the code.
    TRACE(TRACE_ALWAYS,_T(" ** An invalid operation has been performed ** "));
    SYSTEMCHECK(false); // Stop here.
}

// Register the custom callback which will be called in release mode.
SafetyExecutive::GetInstance()->RegisterInvalidOperationCallback(ReportCheats);
// and this callback which will be called when testing in internal or
// debug mode.
SafetyExecutive::GetInstance()
                    ->RegisterInvalidOperationCallback(TraceInvalidOperations);

13.3.3. CallMethodOperation

The major purpose of the CallMethodOperation class is to ensure that embedded RMC calls do not deadlock while waiting for a return value. This is achieved by postponing the RMC call when it is not yet ready to return the result, and then recalling the RMC a short time later. In the meantime, any queued messages are processed so that if the original RMC relies on a queued message, that message is processed and the RMC call can be completed. To postpone an RMC, call the CallMethodOperation::PostponeOperation member function and set the required time delay. If the DOCallContext flag TargetObjectMustBeMaster or TargetObjectMustHaveAuthority is set, it is only guaranteed to be true the first time that the member function is called. If the call is postponed, these flags are not rechecked and may no longer be true. A UserContext may be attached to the operation object using AttachUserContext and retrieved using GetUserContext. GetTargetMethodID returns the MethodID of the RMC that was called, and GetAttemptCount returns the number of times that the operation has tried to complete the RMC call but was postponed.

The following example illustrates one way in which the CallMethodOperation class can be used. In this case, it is being used to compute factorial numbers.

Code 13.14 Sample Usage of the CallMethodOperation Class

// Create a specific context for the Factorial object.
struct FactorialContext: public RootObject
{
    qInt m_iResult;
    RMCContext m_oRMCContext;
};

qInt GameObject::Factorial(qInt iValue, DOHandle hS1, DOHandle hS2)
{
    // Called in the context of an RMC.
    SYSTEMCHECK(GetCurrentOperation()->GetType() == Operation::CallMethod);
    CallMethodOperation* pCurrentOp=CallMethodOperation::DynamicCast(
                                                        GetCurrentOperation());
    if (iValue==0)
    {
        SYSTEMCHECK(pCurrentOp->GetAttemptCount()==1);
        return 1;
    }
    else
    {
        FactorialContext* pContext=NULL;
        // If this is the first time the operation is called
        // we initiate the call.
        if (pCurrentOp->GetAttemptCount()==1)
        {
            // Initiate the call.
            pContext=qNew FactorialContext;
            pCurrentOp->AttachUserContext(UserContext(pContext));

            pContext->m_oRMCContext.ClearFlag(RMCContext::SynchronousCall);

            // Select worker station.
            if (Station::GetLocalStation()==hS1)
            {
                pContext->m_oRMCContext.SetTargetStation(hS2);
            }
            else
            {
                pContext->m_oRMCContext.SetTargetStation(hS1);
            }

            qBool bResult=CallFactorial(&(pContext->m_oRMCContext),
                                &(pContext->m_iResult), iValue-1, hS1, hS2);
            SYSTEMCHECK(bResult);
        }
        else
        {
            pContext=static_cast<FactorialContext*>(
                        pCurrentOp->GetUserContext().GetPointer());
            SYSTEMCHECK(pContext!=NULL);
        }

        QTRACE(TRACE_ALWAYS,(_T("Testing if result of factorial(%d) is ready"),
                            iValue));
        if (pContext->m_oRMCContext.Wait(0))
        {
            // If the result is ready we return the result
            // and delete the created context.
            QTRACE(TRACE_ALWAYS,(_T("Result of factorial(%d) ="), iValue,
                                pContext->m_iResult));
            qInt iResult=iValue * pContext->m_iResult;
            qDelete pContext;
            return iResult;
        }
        else
        {
            // If the result is not yet ready we postpone the
            // operation for 100 ms before recalling the method.
            pCurrentOp->PostponeOperation(100);
            return 0;
        }
    }
}

In addition, the CallMethodOperation class may be used to control how, when, and by whom RMCs are called, and to return a DOHandle to the RMC callee station. All RMCs are uniquely identified by NetZ, enabling particular RMCs to be called or denied according to the station that calls them, or the particular circumstances surrounding the call. For example, you may want to restrict particular calls so that they can only be called by a server station, or only called during the game setup, or at a particular point in the game. For example, you could use the following code to implement restrictions so that some RMCs can only be called on server stations.

Code 13.15 Restrictions That Cause Some RMCs to Only Be Called on Server Stations

qBool RestrictSpecialRMCs(const Operation & oOperation)
{
    static MethodID  idSpecialRMC =MethodIDGenerator::GetID(_T("SpecialRMC"));
    // First we check that the operation being called is
    // a CallMethodOperation and that the RMC is a SpecialRMC.
    if (oOperation.GetType()==Operation::CallMethod &&
            CallMethodOperation::DynamicCast(&oOperation)->GetTargetMethodID()
            ==idSpecialRMC)
    {
        // As only server processes can call this special RMC we
        // check if the station that called it is a server.
        Station::Ref refCallOrigin(oOperation.GetOrigin());
        if (refCallOrigin.IsValid() && refCallOrigin->
                                    GetProcessType()==Station::ServerProcess)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    // All other operations are allowed.
    return true;
}

13.3.4. Session Operations

SessionOperation includes a JoinSessionOperation. A JoinSession operation is used to accept or deny a station's request to join a session JoinSessionOperation.

When a station attempts to join the session, the JoinApproval callback, registered using the Session::RegisterJoinApprovalCallback object specified as an argument, is called to determine whether the request is approved or denied. If approval to join the session is granted, that is, the callback returns JoinSessionOperation::Approve, a JoinSession operation is performed. On the other hand, if the join is denied (that is, the callback returns JoinSessionOperation::Deny), no operation is performed and a message is returned to the joining station confirming that the join request was denied. If no JoinApproval callback is registered or the callback does not call either Approve or Deny, the join request is approved by default. The callback can only be called if the approval state of the join, returned by the JoinSessionOperation::GetApprovalState method, is either Unknown or Pending. This latter state is returned if the decision was postponed by the JoinSessionOperation::PostponeDecision method. The JoinApproval callback and subsequent JoinSession operation are called on the station by which the joining station attempts to join the session.

For example, if you wanted to restrict the number of players in your game to four and only let the players who have the secret password join, you could use the following code to implement the JoinApproval callback.

Code 13.16 Operation to Restrict the Number of Players to Four and Only Allow Players With the Secret Password to Join

void CheckJoiningProcesses(JoinSessionOperation * pOperation)
{
    // Count the number of stations, but skip the
    // tool stations (for example, ObjectMonitor)
    qUnsignedInt uiPlayerCount=0;
    Station ::SelectionIterator it;
    while (!it.EndReached())
    {
        if (it->GetProcessType()!=Station ::ToolProcess)
        {
            uiPlayerCount++;
        }
        it++;
    }

    // Approve the join if the maximum number of
    // players has not been reached and if the joining
    // player has supplied the correct password.
    if (uiPlayerCount<=4 && pOperation->GetJoiningStation()->
                            GetIdentificationToken()==_T("CorrectPassword"))
    {
        pOperation->Approve();
        return;
    }
    // Always approve the join when the station is a tool process.
    // Note that for security purposes, we might want to force
    // tool processes to also use a password, or this section of
    // the code could be disabled in release mode.
    if (pOperation->GetJoiningStation()->GetProcessType()==
                                                        Station ::ToolProcess)
    {
        pOperation->Approve();
        return;
    }
    pOperation->Deny();
}

13.3.5. Operation Callbacks

The OperationCallback class lets you call user-defined callbacks on system operations. Implement your own callback on a particular system operation by subclassing OperationCallback, implementing the CallMethod method, and then registering the callback with the appropriate OperationManager using OperationManager::RegisterCallback.

When an OperationCallback is created, it must be assigned a priority within the range -1024 to 1024, excluding the priorities used by the system. Operation callbacks are then called in numerical order according to their allocated priority. The priority is a positive or negative integer that determines the sequence in which operation-related events occur within the system. All registered callbacks are called from lowest priority to highest priority, with the actual operation taking place at a priority of zero. Priorities enable you to control the order in which your own operation callbacks are called, such as ensuring a callback is called immediately before or after OperationBegin is called, for example. The priorities used by NetZ and the order in which they are called are illustrated in the following image.

_images/Fig_DO_Ext_OperationCallbacks_CallingTimeline.png

Figure 13.2 A Timeline That Indicates the Calling Order for the OperationCallbacks Defined by NetZ

13.4. Forward Compatibility of Duplicated Objects

The DDL definition protocol for duplicated objects is designed with forward compatibility in mind. This section describes issues to be aware of regarding forward compatibility.

13.4.1. Forward Compatibility of the Duplicated Object Class

Duplicated object classes are assigned a class ID in the order they are defined, in the order DDL definition files are loaded, and in the order those classes are defined in the DDL files at time of execution. A duplicated object class can be added as long as a different class ID is used. However, correct operations are not guaranteed if the added class is treated as a WellKnown object or published.

100 or more class IDs for user duplicated objects can be assigned. For forward compatibility, when assigning a class ID out of order, a class ID can be specified using the classid parameter in the DDL. classid can be assigned a value from 100 up to 255.

Code 13.17 Example of Defining classid in a DDL File

doclass A {
};

doclass B {
    classid = 102; //Specifies a value of 102 for the class ID rather than a value of 101, which would be assigned automatically.
};

13.4.2. Forward Compatibility of Remote Method Calls

Remote method calls are assigned an ID in the order they are defined for each class. A unique method ID that includes a class ID is assigned globally. The same method ID used in a parent class is used in its child classes, rather than assigning a new method ID.

This enables you to declare new remote method calls by adding them to end of a duplicated object class when you release a new version of an application. As long as the order of definitions is the same, using different field names in different DDL definition files represents no problem.

If a method that is not defined in order in the DDL of an older application is called from a new version of the application, the RMC call fails and RMCContext::GetOutcome gets DOCallContext::ErrorRMCDispatchFailed.

13.4.2. Forward Compatibility of Datasets

Datasets and member variables of datasets used as member variables by a duplicated object class are assigned a unique ID in the order that each duplicated object class and each dataset is defined. When data is received, data from a dataset that is not defined in order in the DDL file is ignored.

You can add new member variables to a dataset by adding a new dataset declaration to a duplicated object class when you produce new versions of an application. As long as the order of definitions is the same, using different member variable names in different DDL definition files represents no problem.

If a dataset in a DDL using different definitions in different versions of the application is received, you can add REVISION to a dataset declaration as the ID number to determine whether the member variable is valid for the DDL from which it originates. REVISION starts from 0. When REVISION is set for an ID, after that member variables are treated as having the specified REVISION number. REVISION can be specified as a value from 1 to 255 when adding member variables within a dataset.

Code 13.18 Example of a REVISION Definition in a DDL File

dataset Data {
    double x;
    REVISION = 1;
    int32 a;
};

doclass X {
    Data data;
};

The API includes a function for getting the REVISION of a received dataset (DataSet::GetRevision), a macro for getting the REVISION of each member variable of that dataset (DATASET_VALID_REVISION), and a macro for getting whether the received dataset is valid based on its REVISION number.

Code 13.19 Sample Code for Getting REVISION

X::SelectionIterator it;
while (!it.EndReached()) {
    QLOG(EventLog::Info,_T("it->data Revision:")<< it->data.GetRevision() //Get the revision of the received dataset.
         << _T(" x:")<< it->data.DATASET_VALID_REVISION(x) //Get the revision of the member variable, x. (=0) 
         << _T(" valid:")<< it->data.DATASET_VALID_REVISION(x) //Get whether member variable x is valid for the revision of the received dataset.
         << _T(" a:")<< it->data.DATASET_VALID_REVISION(x) //Get the revision of the member variable, a. (=1) 
         << _T(" valid:")<< it->data.DATASET_VALID_REVISION(x) //Get whether member variable a is valid for the revision of the received dataset.
    );
}

CONFIDENTIAL