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.
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)
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
.
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.
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.
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.
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)
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.
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.
DuplicatedObject::Create
function.SafetyExecutive
function is called to determine whether the object may be created. If so, continue. If not, exit.DOHandle
and MasterID
are assigned to the object. The DuplicatedObject::IsADuplica
and DuplicatedObject::IsADuplicationMaster
member functions are now valid.DuplicatedObject::Publish
member function is called.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.DuplicatedObject::InitDO
member function is called.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.When an object is added to the duplica store the following sequence of events occurs.
DOHandle
and MasterID
are assigned to the object. The DuplicatedObject::IsADuplica
and DuplicatedObject::IsADuplicationMaster
member functions are now valid.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.DuplicatedObject::InitDO
member function is called.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.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.
SafetyExecutive
function is called to determine whether the object may be deleted. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called. Iterations can no longer be performed over the duplication master object.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.
SafetyExecutive
function is called to determine whether the object may be deleted. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called. Iterations can no longer be performed over the duplica object.RefTemplate
class on the local station equals 0, the object's destructor is called.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.
SafetyExecutive
function is called to determine whether the object may be created. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
method is called on the duplication master station.AddToStore
operation on that station.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.When a duplica is removed, the following sequence of events occurs.
RemoveFromStore
operation is called on the station where the duplica will be deleted.SafetyExecutive
function is called to determine whether the object may be deleted. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
method is called on the duplication master station.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.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.
SafetyExecutive
function is called to determine whether the object can be migrated. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.StationID
. The role changes that accompany this change are also performed.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.FaultRecovery
FaultRecovery
operation is performed in the following order.SafetyExecutive
function is called to determine whether the object should survive the fault. If so, continue. If not exit.DuplicatedObject::OperationBegin
member function is called.DuplicatedObject::ApproveFaultRecovery
member function is called.If the DuplicatedObject::ApproveFaultRecovery
function returns true
the session master takes control of the duplicated object with the following sequence of events.
ChangeMasterStation
operation is performed.DuplicatedObject::OperationEnd
member function is called.If the DuplicatedObject::ApproveFaultRecovery
function returns false
, the object is removed from the store with the following sequence of events.
RemoveFromStore
operation is performed.DuplicatedObject::OperationEnd
member function is called.RefTemplate
class on the local station equals 0, the object's destructor is called.CallMethodOperation
A CallMethodOperation
occurs as a result of an RMC being called, when the following sequence of events occurs.
SafetyExecutive
function is called to determine whether the call may proceed. If so, continue. If not, exit.DuplicatedObject::OperationBegin
member function is called.DuplicatedObject::OperationEnd
member function is called. If an RMC is postponed, then when it is recalled it passes again by using the SafetyExecutive
function before the operation begins.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.
SafetyExecutive
function is called to determine whether the object can be updated. If so, continue. If not, exit.DataSet::OperationBegin
member function is called.DuplicatedObject::OperationBegin
member function is called.DataSet::OperationEnd
member function is called.DuplicatedObject::OperationEnd
member function is called.JoinSession
When a station attempts to join a session, the following sequence of events occurs.
JoinApproval
callback set by Session::RegisterJoinApprovalCallback
is called.JoinApproval
callback returns JoinSessionOperation::Approve
, then the station proceeds to join.JoinApproval
callback returns JoinSessionOperation::Deny
, then the station requesting to join the session is informed that the request has been denied. JoinStation
When JoinSessionOperation::Approve
is returned by the JoinApproval
callback, the following next station join-in processes occur.
JoinStationOperation
starts.JoinStationOperation
terminates.LeaveStation
When a station leaves the session, the following sequence of events occurs.
RemoveFromStoreOperation
.LeaveStationOperation
starts.LeaveStationOperation
terminates.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);
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;
}
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();
}
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.
Figure 13.2 A Timeline That Indicates the Calling Order for the OperationCallbacks
Defined by NetZ
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.
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.
};
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
.
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