bw logo

Chapter 6. Methods

Methods allow events to be propagated, both between different execution contexts of an entity (i.e., cell, base, client), as well as between different entities. BigWorld separates entity methods into categories based on the execution context within which they will be executed.

In general, methods should not be used for propagating states. The use of properties is recommended for this purpose. For example, a player holding a gun should be a property, while a player shooting should be a method.

The categories of methods are:

Category Runs on Common uses
<BaseMethods> BaseApp

Updates properties on the base.

Serves as a root point to propagate messages to related things.

<CellMethods> CellApp

Notifies the cell of changes in response to player interaction.

Allows communication between nearby entities.

<ClientMethods> Clients

Notifies the client of events, so that the player can see them. Implicit set_<property_name> methods need not be declared.[a].

Method categories.

The grammar for method declaration is described below:

<[ClientMethods|CellMethods|BaseMethods]>

   <method_name>
      <Exposed/>

      <Args>
         <arg1_name> data_type </arg1_name>
         <arg2_name> data_type </arg2_name>
      </Args>

      <ReturnValues>
         <ret1_name> data_type </ret2_name>
         <ret2_name> data_type </ret2_name>
      </ReturnValues>

      <!-- Only send one call to clients if called repeatedly -->
      <SendLatestOnly> [true|false] </SendLatestOnly> 1

      <!-- Send the call reliably -->
      <IsReliable> [true|false] </IsReliable> 2
   </method_name>

</[ClientMethods|CellMethods|BaseMethods]>

<res>/scripts/entity_defs/<entity>.def Method declaration syntax

6.1. Basic Method Specification

All methods in all categories have some fundamental common characteristics. They are declared in the relevant section in the file <res>/scripts/entity_defs/<entity>.def, with an XML tag per method.

The method's arguments and return values (if any) are also defined in the file, and its types are specified in the same way as property types. For more details, see Property Types.

In order to declare a method called yell on the cell, which receives a string argument named phrase, we would have the lines below:

<root>
  ...
  <CellMethods>
    <yell>
      <Args>
        <phrase> STRING </phrase> <!-- phrase to exclaim -->
      </Args>
    </yell>
  </CellMethods>
  ...
</root>

<res>/scripts/entity_defs/<entity>.def Declaration of cell method yell

Once the method is declared, it also needs to be declared in the appropriate Python implementation file. Each context of execution (cell, base, and client) has a folder containing scripts for each entity.

In our example, the method was added to the section <CellMethods>, and therefore will be executed on the cell entity.

The cell script for this entity, named <res>/scripts/cell/<entity>.py, will need to define the yell method, as illustrated below:

import BigWorld
...
class <entity>(BigWorld.Entity):

  def __init__(self):
    BigWorld.Entity.__init__(self)

  def yell(self, phrase):
    # insert code to implement yell here
    return

<res>/scripts/cell/<entity>.py Definition of cell method yell

6.2. Two-way calls

Remote two-way method calls can be used to get return values from Python calls between processes. Return values for entity methods are declared in the same format as the method arguments in the entity definition file, except that they are listed under the ReturnValues tag instead of Args. For example, the webCreateAuction Base method for an Avatar has the following definition:

<webCreateAuction>
  <Args>
    <itemSerial>  ITEMSERIAL  </itemSerial>
    <expiry>      UINT32      </expiry>
    <startBid>    GOLDPIECES  </startBid>
    <buyout>      GOLDPIECES  </buyout>
  </Args>
  <ReturnValues>
    <auctionID>   AUCTIONID   </auctionID>
  <ReturnValues>
</webCreateAuction>

This method takes a number of arguments containing the details of the item being listed for auction, and returns the unique auction ID. However, this method makes a remote call to an entity that is potentially on another process, which prevents us from receiving a return value in the usual way. Instead, this method will return a Twisted Deferred object, that will send the return values as an argument to a callback method once they are received. This entire process is explained in Twisted Deferred Objects.

6.2.1. Twisted Deferred Objects

To obtain the return value of a two-way method, Twisted Deferred objects are used. When a method is called, a Deferred object is returned. Two types of callback methods can then be added to this object: a callback for successful calls, and an errback for when an an error has occured. When the method has been executed, it will call one of the two callback methods, depending on the success of the call. It is also possible to chain more than one callback for both success and failure.

For a more detailed explanation about how Deferred objects are used, see the Twisted documentation at http://twistedmatrix.com/documents/current/core/howto/defer.html.

As an example, we will implement remoteTakeDamage, a Base method for an Avatar that causes it to lose some health, and returns its remaining health. This method will call takeDamage, one of the Avatar's Cell methods.

takeDamage will take the damage amount as its argument, and return two values: the entity's remaining health, and its maximum health. These values will be printed, and the remaining health amount will be returned from remoteTakeDamage as the Deferred object's its final value.

Since the Base entity and Cell entity for the Avatar are on different processes, the call will use Deferred objects to effectively achieve an asynchronous two-way call. The methods will have the following definitions in the entity definition file, Avatar.def:

...
    <BaseMethods>
        <remoteTakeDamage>
            <Args>
                <damage>        INT32   </damage>
            </Args>
            <ReturnValues>
                <health>        INT32   </health>
            </ReturnValues>
        </remoteTakeDamage>
        ...
    </BaseMethods>

    <CellMethods>
        <takeDamage>
            <Args>
                <damage>        INT32   </damage>
            </Args>
            <ReturnValues>
                <health>        INT32   </health>
                <maxHealth>     INT32   </maxHealth>
            </ReturnValues>
        </takeDamage>
        ...
    </BaseMethods>

remoteTakeDamage has the following implementation in fantasydemo/res/scripts/base/Avatar.py:

# base/Avatar.py

from twisted.internet import defer
...

def remoteTakeDamage( self, damage )
    deferred = self.cell.takeDamage( damage )
    deferred.addCallback( partial( self.onDamageTaken, damage ) )
    return deferred

def onDamageTaken( self, damage, healthArgs ):
    health, maxHealth = healthArgs[ 0 ], healthArgs[ 1 ]
    print "Avatar 's' took %d damage, reducing its health to %d/%d" % \
        (self.basePlayerName, damage, health, maxHealth)
    return (health,)

Since the call to takeDamage is asynchronous, we will not receive the return value immediately. Instead, a Deferred object will be returned, referred to by deferred. Once a result is available, it will be stored in the Deferred object, taking the form of a tuple containing the return values. Since we are unable to receive and use the values immediately, we can specify a method to be called using the values as an argument, once they become available.

The addCallback method is used to specify a method that will be called once this result is available. This method will be given the result tuple as its argument. In our example, once the result is available in the Deferred object, it will be sent as the final argument to the onDamageTaken method. If additional callback methods are specified using addCallback, they will be called in turn. The result from each one will be stored as the new result in the Deferred object, and sent as the argument for the next callback method in the list. This is called a 'callback chain'.

When there are no remaining callback methods in the chain, the final result will be stored in the Deferred object. At this point, any additional callbacks that are added will be called immediately, since there is already a result available.

Finally, the result of onDamageTaken will be stored in the Deferred object, and sent to the caller of remoteTakeDamage.

Methods can also be specified to be called if a remote method call fails. This is done using the addErrback method, for example:

deferred.addErrback( onError )

If an error object is returned from the call, it will be used as the argument to onError.

Errbacks can be chained in the same way as callbacks, by using multiple calls to addErrback. The addCallbacks method adds both a callback and an errback to a deferred object, but it has slightly different behaviour than adding the two separately using addCallback and addErrback. addCallbacks adds both methods at the same level in the call chain, whereas adding them separately will put each method on its own level. This is explained in detail in the Twisted documentation, at http://twistedmatrix.com/documents/current/core/howto/defer.html.

The implementation of takeDamage, in cell/Avatar.py, is as follows:

# cell/Avatar.py

import CustomErrors
from twisted.internet import defer
...

def takeDamage( self, damage ):
    if (damage < 0):
        # Goes to errback which 
        return defer.fail( CustomErrors.InvalidFieldError(
                "Avatar can not take negative damage" ) )

    self.health = self.health - damage

    if (self.health < 0):
        self.health = 0

    # Goes to onDamageTaken callback
    return (self.health, self.maxHealth)

Methods that are called in this way must return either a tuple containing the return values, or a Deferred object. If a tuple is returned from takeDamage, its values will be stored in deferred, in the remoteTakeDamage method, and sent as arguments into its callback method, onDamageTaken, as described above. However, if a Deferred object is returned, its result tuple and any callbacks specified for it will be propagated to remoteTakeDamage's deferred object, and the onDamageTaken callback will be added to the end of the chain.

For examples of remote methods that create Deferred objects, add callbacks and errbacks to them and return them, refer to the web interface Base methods in FantasyDemo's Avatar and AuctionHouse entities, located at fantasydemo/res/scripts/base.

In our implementation of takeDamage, an error results in a CustomErrors.InvalidFieldError object (a custom error derived from Exception) to be returned using defer.fail. This method creates a special error object: a Deferred object that has already had an errback called. Using this method is a simple way of returning an error to the original caller. Once the error object is received by the original caller, it will be used as the argument for the first errback method, if there is one. Instead of calling defer.fail, an exception can be raised:

raise CustomErrors.InvalidFieldError( "Avatar can not take negative damage" ) )

This will have the same result as returning the error object using defer.fail, and will also send the call stack for the error to MessageLogger.

In this example, we use CustomErrors objects. These custom errors are derived from BWError, and are the recommended way to send errors in two-way calls. For details about custom errors, refer to Error objects.

6.2.2. Error objects

If a two-way call fails, it will send an error object to the Deferred object, and invoke the first errback method, if there is one specified. This object is an instance of an Exception.

A number of error types deriving from our custom exception class BWError are defined in bigworld/res/scripts/server_common/BWTwoWay.py. These can be extended to suit the errors that could come about in the game scripts. Two subclasses of BWError are defined: BWStandardError, which is primarily used as a base class for errors arising in the server binaries, and BWCustomError, which is used to as a base class for errors occuring in the game scripts.

6.2.2.1. BWStandardError

Error objects originating in the server binaries are derived from the BWStandardError type, and are declared in bigworld/res/scripts/server_common/BWTwoWay.py. These error types are as follows:

  • BWAuthenticateError

    A player can not be authenticated with the credentials they provided.

  • BWInternalError

    The server experienced an error that was caused internally.

  • BWInvalidArgsError

    A method is called with invalid arguments.

  • BWMercuryError

    No response was received for a request.

  • BWNoSuchCellEntityError

    A cell entity can not be found.

  • BWNoSuchEntityError

    An entity can not be found.

6.2.2.2. BWCustomError

New error types should be implemented to allow errors to be as helpful as possible. BWCustomError should be used as the base class for a range of error classes specific to the game scripts. For example, FantasyDemo defines a number of custom error classes for its two-way calls specific to its auction house implementation, such as InsufficientGoldError and InvalidItemError. For a description of each of the custom error types used in FantasyDemo, refer to the Server Web Integration Guide's sectionPHP Error handling.

Custom errors are declared in <res>/scripts/server_common/CustomErrors.py. The above implementation of remoteTakeDamage shows how a custom error can be returned as the deferred object instead of the return values:

return defer.fail( CustomErrors.<Error Type>( <Args> ) )

6.3. Service Methods

Methods for services are defined in the much same way as base, cell and client methods. However, there are some slight differences, since, unlike the other categories, services are not entities. Service methods are declared in the file <res>/scripts/service_defs/<service>.def, with an XML tag per method.

Service methods fall under the Methods tag, instead of under a specific category such as BaseMethods or CellMethods.

In order to declare a method called addNote on the service, which receives a string argument named newNote, we would have the lines below:

<root>
  ...
  <Methods>
    <addNote>
      <Args>
        <newNote> STRING </newNote>
      </Args>
      <ReturnValues>
        <noteID> NOTE_ID </newNote>
      </ReturnValues>
    </addNote>
  </CellMethods>
  ...
</root>

<res>/scripts/service_defs/<service>.def Declaration of service method addNote

Once the method is declared, it also needs to be implemented in the service Python implementation file. The service execution context has a directory containing the scripts for each service.

The script for this entity, named <res>/scripts/service/<service>.py, will need to define the addNote method, as illustrated below:

import BigWorld
...
class <service>( BigWorld.Service ):

  def addNote( self, description ):
    # insert code to implement addNote here
    return

<res>/scripts/service/<entity>.py Definition of service method addNote

6.4. Intra-Entity Communication

Different execution contexts of an entity communicate with each other by calling methods on the other execution contexts. These are exposed as special properties of the entity.

As a quick reference, the available objects are described below:

  • allClients Available on: Cell

    When/how to use: To call a client method on all client instances of this entity.

    Example: self.allClients.someMethod()

  • base Available on: Cell, Client

    When/how to use: To call a base method of this entity. Calls to this object are executed on the base script. The client cannot directly call methods on the base of other entities.

    Example: self.base.someMethod()

  • cell Available on: Base, Client

    When/how to use: To call a cell method of this entity. Calls to this object are executed on the cell script. All client instances can access their cell object (when the entity exists on the cell).

    Example: self.cell.someMethod()

  • otherClients Available on: Cell

    When/how to use: To call a client method on all client instances of this entity, except on its own.

    Example: self.otherClients.someMethod()

  • ownClient Available on: Cell, Base

    When/how to use: To call a client method only on this entity's client. This object calls the method only on the entity on the client application that is 'playing' as this entity, not on other client applications that can see this entity.

    Example: self.ownClient.someMethod()

The methods of a nearby entity can be called from the client directly on the cell part of that entity.

BigWorld automatically exposes these objects to the relevant script classes.

What this means is that it is possible for any script that is part of an entity (cell, base or client part) to call other scripts that are part of the same entity. The definition file (<res>/scripts/entity_defs/<entity>.def) describes which methods are exposed to different execution contexts.

6.5. Bandwidth Optimisation: Send Latest Only

When a method is called on Entity.otherClients (or Entity.allClients), an event object is created and added to the event history. This event history is used when updating client applications that have this entity in their AoI. When this entity is considered[7], any events that have been added since the last time this entity was considered are sent to the client. There is the potential for multiple calls of a single method to be sent in a single update.

If the SendLatestOnly flag is set on a client method, only the latest call is kept in the event history. This avoids sending multiple calls. This can save bandwidth being sent to the client and can save some memory on CellApps if a method is called frequently and only the latest call is needed.

This value is false by default.

Properties with the OTHER_CLIENTS flag also have this flag. See Bandwidth Optimisation: Send Latest Only.

6.6. Bandwidth Optimisation: Is Reliable

When a method is called on Entity.otherClients (or Entity.allClients), a message is sent to the appropriate client applications that have this entity in their AoI[8]. By default, these messages are sent reliably. If the IsReliable flag is set to false on a client method, this message will be sent unreliably.

Properties with the OTHER_CLIENTS flag also have this flag. See Bandwidth Optimisation: Is Reliable.

6.7. Sending Auxiliary Data to the Client Via Proxy

Auxiliary data can be streamed to the client via the proxy, without affecting the normal game traffic. This data is opportunistically streamed to the client when bandwidth is available.

The bandwidth used by auxiliary data streaming can be controlled using the bw.xml options in <baseApp/downloadStreaming>. For details of this option group refer to the Server Operations Guide section BaseApp Configuration Options.

All data types in this streamed data are user-defined, since BigWorld does not have any internal uses for it.

Data is added to a proxy via method streamStringToClient:

id = Proxy.streamStringToClient( id, data )

or via method streamFileToClient:

id = Proxy.streamFileToClient( id, resource )

Where the parameters are:

  • id

    16-bit ID of the data. If -1 is received, then the next ID in sequence that is not currently in use is selected. The caller of this method is responsible for the management of this parameter. The same ID value used by the method is returned to the caller.

  • data

    Data to be sent to the attached client. Must be in string format.

  • resource

    The string name of the resource to be sent to the client.

Once the client has received the entire data string, the callback BWPersonality.onStreamComplete is called on the client.

6.8. Exposed Methods Client-to-Server Communication

Because MMOGs operate over the Internet, and in order to stop players cheating, server methods (those on the cell or the base) are not automatically allowed to be called by the client.

In order to make a server method callable from the client (so that the world can provide interactivity), its declaration has to include the tag <Exposed/>, as illustrated below:

<root>
  ...
  <CellMethods>
    <yell>
      <Exposed/>
      <Args>
        <phrase> STRING </phrase> <!-- phrase to exclaim -->
      </Args>
    </yell>
  </CellMethods>
  ...
</root>

<res>/scripts/entity_defs/<entity>.def Declaration of the exposed method

The tag <Exposed/> accomplishes two things:

  • It makes the method available to clients.

  • On the cell, it acts as an additional argument tag that is automatically filled in with the entity ID of the client calling it.

Client instances actually call the method with one argument fewer than the number received by the cell entity, which prevents 'entity-faking' outside the safe server environment. The entity ID needs to be passed as an argument when calling exposed methods from the server components.

The definition of the method on the cell must be extended to take this parameter, as illustrated below:

import BigWorld
class EntityName(BigWorld.Entity):

  def __init__(self):
    BigWorld.Entity.init(self)

  def yell(self, sourceEntityID, phrase):
    # insert code to implement yell here
    # if desired, check that self.id == sourceEntityID before proceeding
    return

<res>/scripts/cell/<entity>.py Definition of cell method yell

On the client, the method yell can be called with the code below:

self.cell.yell( "BigWorld message test" )

Example of a client calling a cell method yell

If the cell method yell implements the check of sourceEntityID against self.id, only its client will be able to call it. Others clients will not be able to execute it.

There might be occasions where the method might run with a different sourceEntityID. For example, for a method called shakeHand, it is a good idea to check that the source entity is within a couple of metres away from the self entity before proceeding (and maybe that neither is dead).

6.8.1. Security Considerations of Exposed Methods

Script writers should be aware that the arguments of any exposed method need to be heavily scrutinised on the server side before being operated on.

The underlying C++ code that handles the passing of arguments ensures that the method will be invoked on the server side only if:

  • The right number of arguments is being passed.

  • The arguments have the expected type.

Beyond that, however, the underlying architecture cannot provide any further constraints on the values that clients may pass to method calls on the server.

For example, integers may take any valid 32-bit value, strings and arrays may be of any length, …. It is up to the script writer to ensure that the arguments to an exposed method have values that make sense in the context of that method.

You should carefully inspect the value of any arguments to an exposed method before using them.

It is never safe to trust arbitrary Python objects from the client as passed in through PYTHON parameters to exposed method invocations. A WARNING log message is emitted at startup by any component that parses the entity definitions if an exposed method has parameter of type PYTHON. The safer alternative is to use some other data type that is more concretely defined, for example the FIXED_DICT data type, and validate each known element.

6.9. Server to Client bandwidth usage of Method calls

The arguments of a method call made from the server to the client are optimised in the same way property updates are optimised.

See Server to Client bandwidth usage of Property updates.

6.10. Client callbacks on property changes

When the server changes a property on the client a callback method is called on the entity to allow the client to take appropriate action.

set_<property_name> is called when a property of an entity is replaced with a new value. That is, a value in the entity's __dict__ is replaced.

If an existing property is modified, either setNested_<property_name> or setSlice_<property_name> is called.

6.10.1. Implicit set_<property_name> Methods

When the server updates a property with distribution flag [9] ALL_CLIENTS, OTHER_CLIENTS or OWN_CLIENT, an implicit method called set_<property_name> is called on the client.

This method should not need be declared in the section <ClientMethods> of the definition file as it is automatically provided by BigWorld.

All implicitly defined set_<property_name> methods have one argument which receives the old value of the property when the method is called. For example:

class Seat( BigWorld.Entity ):
    ...
  def set_seatType( self, oldValue ):
    ...

<res>/scripts/client/Seat.py Example of an implicit set_seatType for the Seat entity.

Note that if an existing property is modified instead of being replaced and the appropriate setNested_<property_name> or setSlice_<property_name> method does not exist, this method is called with the oldValue argument as None.

6.10.2. Implicit setNested_<property_name> Methods

If a nested property of an existing property is modified, the setNested_<property_name> is called. This includes modifying a single element of an ARRAY or FIXED_DICT. The method accepts two arguments. The first represents the path to the change and the second is the value that has been replaced.

For example:

class Seat( BigWorld.Entity ):
    ...
  def setNested_myFixedDict( self, path, oldValue ):
    ...

The first argument represents the path to the changed property. For example, if the change was:

self.myFixedDict.rightHandItem.weight = 10

The value of path would be:

["rightHandItem", "weight"]

If you had a property that was ARRAY <of> ARRAY <of> INT32 </of> </of>.

self.myArray[ 5 ][ 3 ] = 8

would result with the value of path being:

(5, 3)

6.10.3. Implicit setSlice_<property_name> Methods

If a slice of an array is modified, the setSlice_<property_name> is called. This includes appending and deleting from an array. The method accepts two arguments. The first represents the path to the changed slice and the second is the slice that has been replaced. The last value in the path is a tuple containing two integers. These represent the range of the new values. To create a slice containing the new values, use myModifiedArray[ path[-1][0] : path[-1][1] ].

For example, if self.myArray is an existing array with 5 elements:

self.myArray.append( 10 )

would result in setSlice_myArray being called with:

path = [(5,6)]
oldValues = [ ]
newValues = self.myArray[ path[-1][0] : path[-1][1] ]

If the array is part of a FIXED_DICT and the array has 5 elements with the last value 21.

del self.myFixedDict.myArray[-1]

would result in:

path = ["myArray", (4,4)]
oldValues = [21]

6.11. LOD on Methods

Like properties, some methods need to be broadcast only to nearby entities.

For example, even though an entity may be visible at an AoI distance (say, 500 metres), it seems unlikely that players will be able to tell the difference between a smiling and a non-smiling entity.

To reflect his, the smile method's level of detail, can be declared as illustrated below:

<root>
  ...
  <ClientMethods>
    ...
    <smile>
      ...
      <DetailDistance> 30 </DetailDistance>
    </smile>
    ...
  </ClientMethods>
  ...
</root>

<res>/scripts/cell/<entity>.py Declaration of client method smile

The specification of the LOD for a method is far simpler than it is for a property. This is because a non-broadcast property change might have to be sent later if the LOD increases, while a non-broadcast method in the same scenario will not have to.

Consequently, the method just needs to declare a tag <DetailDistance> inline, and when it is called on exposed objects allClients or otherClients, it is broadcast only to clients within the specified distance around the entity.

6.12. Inter-Entity Communication

Once the entities have methods assigned to them, it becomes useful to be able to call methods on other entities.

If you have an object ent as a Python script representation of another entity, then you can call ent.someMethod() to call that method on ent. This assumes that someMethod() runs on the same execution context you are in. For example, if you are on the cell, then ent.someMethod() must be defined and provided by the entity type of ent.

If you are on the cell, you can call a method on the base, with the code below:

ent.base.otherMethod()

This means that once you are able to obtain an entity object, there is a plethora of options for invoking methods on different execution contexts of different entity instances. For more details, see Intra-Entity Communication.

But to achieve this, first it is necessary to retrieve the object for another entity. The mechanism for that is described in the next sub-section

6.12.1. Entity IDs

In order to uniquely identify every object in the game universe, BigWorld assigns a unique number to every entity. This is referred to as the entity ID.

In the BigWorld Python module (which is imported at the start of most scripts), there is an object which maps entity IDs to the corresponding entity object. This object has the same interface as a Python dictionary, and is called BigWorld.entities.

Given an entity id entityID, its entity object can be retrieved with the code below:

ent = BigWorld.entities[ entityID ]

Retrieval of entity object using its ID

One should be very careful with entity IDs, and always check for the existence of the corresponding entity before assuming that it is safe to use it. BigWorld.entities throws an exception if the entity looked up does not exist.

Each execution context has a version of the BigWorld.entities object with different entities in it.

BigWorld.entities contains the entities that are relevant to the execution context in which it is located, as listed below:

Execution context Entities listed in BigWorld.entities
Cell Real and ghosted entities located on all cells managed by this cell's CellApp.
Base Entities located on this base's BaseApp [a].
Client Entities in the client's AoI.

[a] For sending messages to bases on other BaseApps, see Mailboxes

Entities listed in BigWorld.entities per execution context.

Entity IDs can be obtained in various ways:

  1. From exposed methods (client sends entity ID as argument).

  2. From the entity object's id property.

    You can pass the object's ID from one execution context to another (e.g., from the client to the cell), so that the other context can obtain the corresponding object. You can also use this property to obtain the ID of a newly created entity.

  3. From various utility methods that one can use to find entity references.

    One of these methods is entitiesInRange.[10]This method is defined for every cell entity, and returns all entity objects located within a certain distance from the calling entity. The resulting output can be queried again for more specific search results.

    For example, to find all the Guard entities within 100 metres of the current entity, you could have the code below:

    def findGuards():
      output = []
      for entity in self.entitiesInRange( 100 ):
        if entity.__class__.__name__ == "Guard":
          output += [entity]
      return output

    <res>/scripts/cell/<entity>.py Obtaining ID of surrounding entities

    Care should be taken when using this approach for entity discovery as it is a linear search, and hence does not scale well. Specifically to be avoided are searches over large entity sets that could be obtained from a search of large distances, or from using the complete set of entities in BigWorld.entities. For a small distance however, one would not expect large numbers of objects (although this depends on the game).

6.12.2. Retrieving Services

While an entity is identified by its entityID, a service is identified simply by its name, regardless of how many ServiceApps are offering it. Each Service instance is referred to as a service fragment.

In the BigWorld Python module, there is an object which maps the name of a service to the corresponding service fragment on one of the ServiceApps that offer it. This object is called BigWorld.services and, like BigWorld.entities, it has the same interface as a Python dictionary.

The service serviceName can be retrieved using the code below:

serviceMailbox = BigWorld.services[ serviceName ]

Retrieval of service using its name

A service retrieved in this way will be a mailbox to the corresponding service fragment. For details about mailboxes, refer to Mailboxes. Once this mailbox is retrieved, the service's methods can be invoked. For example:

BigWorld.services[ 'ExampleService' ].myTest( u"This is a test" )

6.13. Mailboxes

Mailboxes are used to communicate with entities that are remote, i.e., not on the current process.

Entities can only access other entities that are running in the same execution context[11]However, it is often useful to be able to send a message to entities in other execution contexts. For example, an entity may wish to send a message to all members of a chat channel, but the channel might not have all its members located on the same BaseApp as the executing entity.

Mailboxes are used to implement the following properties of an entity:

  • self.cell

  • self.base

  • self.ownClient

There are also a special set of mailboxes available for use from the cell which reference other entities in relation to the current entity. These mailboxes are discussed in Special Mailboxes.

These afore mentioned properties (cell, base, ownClient) allow you to reference objects located on different processes, and can be sent like any other value, using method calls, and the MAILBOX data type.

You can use the MAILBOX like a normal entity reference, and call methods declared in the entity's definition file on other entities on other processes.

Considering that an entity B (referenced in the following example as anotherEntity) has a method heyThere which takes one argument of type MAILBOX, an entity A can pass its base mailbox to entity B from the cell, as in the example below:

anotherEntity.heyThere( self.base )

<res>/scripts/cell/<original_entity>.py Passing base mailbox from cell

On the receiving entity B (referenced in the example as anotherEntity), the calling entities (entity A) base mailbox (as received via the method argument) can be used to call a method someMethod on entity A. For example:

def heyThere( self, originalEntity ):
  originalEntity.someMethod()

<res>/scripts/cell/<another_entity>.py Receiving base mailbox

It is also possible to store mailboxes in a class as properties. However this is only useful for base entity mailboxes, since they will not change address as the entity moves around the space.

Cell entity mailboxes should not be stored, because they can change as the entity moves between cells. It is possible to pass cell entity mailboxes to another entity for once-off use, since they are guaranteed to be usable for the time it takes to call a method and have it respond (i.e., up to approximately 1 second).

Using mailboxes, it is also possible to call methods within a different execution context to that of the mailbox being called. For example, using a base mailbox of an entity (baseMB in the following code), it is possible to call a cell method of the base entity as follows:

baseMB.cell.cellMethod()

Calling a cell method of another entity through its base mailbox

The call is sent to the base entity first, which then calls the cell method from where the base entity resides. Though it is more convenient than calling a base method that in turn calls the cell method, it still takes two hops to call the cell entity.

Available usages are:

  • baseMB.cell

  • baseMB.ownClient

  • cellMB.base

  • cellMB.ownClient

These are in fact instances of MailBoxes, and can be passed as method arguments. However, the same restriction applies to cellMB.base and cellMB.client as to cell mailboxes they can change as an entity moves around, and therefore should not be stored for later use.

Note that both baseMB.ownClient and cellMB.ownClient refer to their own client only. There are no shortcut calls to otherClients and allClients.

A convenient way to obtain other entities' mailboxes is by using the methods BigWorld.lookUpBaseByDBID and BigWorld.lookUpBaseByName on the BaseApp. For more details, see the BaseApp Python API's entry Main Page BigWorld (Module) Member Functions.

6.13.1. Special Mailboxes

On top of the previously discussed mailboxes, the CellApp also offers a special set of mailboxes which can be used to communicate with entities within the AoI of the reference entity.

The available mailboxes are:

  • entity.allClients

  • entity.otherClients

  • entity.clientEntity( EntityID )

The allClients mailbox will call a method on all client entities that exist in the AoI of the reference entity as well as on the reference entity's ownClient.

The following diagram illustrates a call to the allClients mailbox on Entity A such as A.allClients.chat(). This results in the chat() method being called on Entity A on ClientApps A, B and C.

The otherClients mailbox, is almost identical to the allClients mailbox, except that the method being called will not be executed on the reference entity's ownClient. This is useful for situations where the reference entity's client was the initiator of the action.

The following diagram illustrates a call to the otherClients mailbox on Entity A such as A.otherClients.jump(). This results in the jump() method being called on Entity A on ClientApps B and C.

The most complicated mailbox is clientEntity( EntityID ). This mailbox is used to call a method on an entity that is represented on the reference entity's ClientApp. This is useful for causing a behaviour that is specific to a single Client to only be seen by the interested client, such as a quest giver talking to the player.

The following diagram illustrates a call to the clientEntity mailbox on Entity B providing the EntityID of C such as B.clientEntity( C.id ).wave(). This results in the wave() method being called on Entity C on ClientApp B.

6.14. Method Execution Context

All entity method calls across physical machines are asynchronous. For example, if you execute self.cell.cellMethod() or self.client.clientMethod() on a base entity, the call returns immediately without any value. The actual method execution takes place on the machine where the cell or client part of the entity resides.

To inform the calling entity of the execution result you will have to call a function from within the method.

The example below describes the client entity of class Avatar initiating a sequence of actions to open a door, executing the following steps:

  1. The client method openDoor of class Avatar calls its cell method openDoor.

  2. That method then calls the cell method unlock of class Door, passing self as an argument. This way, Door receives a mailbox of the cell entity Avatar, which is later used (with exposed object client) to call the appropriate cell method on Avatar.

  3. That method then checks the keycard, and using the cell mailbox (sourceEntity), makes a call directly to the appropriate client method of class Avatar (in this example, we assume it was successful).

  • The client entity of the Avatar class:

    class Avatar( ):
      ...
      def openDoor( self, doorID ):
        # Call the cell method to open the door
        self.cell.openDoor( doorID )
      ...
      def doorOpenFailed( self, doorID, keycard ):
        # Animation shows the Avatar scratching his head
      ...
      def doorOpenSucceeded( self, doorID, keycard ):
        # Animation shows the door with corresponding doorID opening
      ...

    <res>/scripts/client/Avatar.py Definition of door methods

  • The cell entity of the Avatar class:

    class Avatar( ):
      ...
      def openDoor( self, doorID ):
        # locate the door
        door = self.locateTheDoor( doorID )
        keycard = self.getKeycardFromInventory()
        door.unlock( self, keycard )
       ...

    <res>/scripts/cell/Avatar.py Definition of methodopenDoor

  • The cell entity of the Door class:

    class Door( BigWorld.Entity ):
      ...
      def unlock( self, sourceEntity, keycard ):
      # check source is close enough
      # check keycard is good
      if not self.isGoodKeycard( keycard ):
        sourceEntity.client.doorOpenFailed( self.id, keycard )
      else:
        self.isOpen = True
        sourceEntity.client.doorOpenSucceeded( self.id, keycard )
      ...

    <res>/scripts/cell/Door.py Definition of methodunlock

The same callback technique is used to return values from a called method.

The example below describes the client entity of class Avatar initiating a sequence of actions to inquire about an item's description based on its inventory index, executing the following steps:

  1. The client method investigateInventory of class Avatar calls its base method itemDescriptionRequest.

  2. That method then calls its client method itemDescriptionReply.

  3. That method then displays the item's description.

  • The client entity of the Avatar class:

    class Avatar( ):
      ...
      def investigateInventory( self, indexInInventory ):
        # first get the details from the server
        self.base.itemDescriptionRequest( indexInInventory )
        # maybe have a timeout in case server doesn't reply
      ...
      def itemDescriptionReply( self, indexInInventory, desc ):
        # call the callback
        if desc == []:
          GUI.addAlert( "No such item" + str(indexInInventory) )
          return
        GUI.displayItemWindow( indexInInventory, desc )
      ...

    Example <res>/scripts/client/Avatar.py Definition of inventory methods

  • The base entity of the Avatar class:

    class Avatar( ):
      ...
      def itemDescriptionRequest( self, indexInInventory ):
        try:
          desc = self.generateDescription( indexInInventory )
        except:
          desc = []    # in case no such index
        self.client.itemDescriptionReply( indexInInventory, desc )
      ...

    Example <res>/scripts/cell/Avatar.py Definition of itemDescriptionRequest

For those entities residing in the same process, the method calls take place synchronously. However, since there is no guarantee that the calling entities and the called ones will always be in the same process, it is better to adopt the callback solution.

A special case is an entity method that is not defined in the entity's definition file (<res>/scripts/entity_defs/<entity>.def For details, see The Entity Definition File). In this case, the method is always executed synchronously, and is only executed in the process of the caller. For example, a method call on a ghosted entity normally will be delegated to the real one. However, if this method is not defined in the entity's definition file, it will be treated as a usual Python method, and run locally.

This mechanism does save a bit of network traffic between server components, and can return a result immediately to the caller, but it is also limited in that it can only access the read-only ghosted properties. Trying to access non-ghosted properties or to write read-only properties would result in unexpected errors. Unless carefully planned, one should not take advantage of this feature.



[7] For more details on how the AoI priority queue works, see Player AoI Updates.

[8] For more details on how the AoI priority queue works, see Player AoI Updates.

[9] For details on property's distribution flags, see Data Distribution

[10] For other methods, please refer to the BigWorld.Entity class reference in the Base, Cell and Client Python API documentation.

[11] For more details, see Inter-Entity Communication