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 which execution context they will be executed within.

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:

<title>Method categories.</title>
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].

The grammar for method declaration is described below:

<[ClientMethods|CellMethods|BaseMethods]>

  <method_name>
    <Exposed/>
    <Arg> data_type </Arg>
   </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 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, we would have the lines below:

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

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

By convention, the meaning of each argument is written next to it as an XML comment.

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. 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.3. 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. Proxy data is opportunistically streamed to the client when bandwidth is available.

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

Data is added to a proxy via method addProxyData:

id = Proxy.addProxyData( id, data )

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.

Once the client has received the entire data string, the virtual method onProxyData in the ServerMessageHandler interface is invoked.

6.4. 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/>
      <Arg> STRING </Arg> <!-- phrase to exclaim -->
    </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 <Arg> tag that is automatically filled in with the entity ID of the client calling it.

Client instances actually call the method with one argument less than are 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.4.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.5. Implicit set_<property_name> Methods

When the server updates a property with distribution flag [6] 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, oldType = None ):
		...

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

6.6. 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 (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.7. 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.7.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:

<title>Entities listed in BigWorld.entities per execution context.</title>
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

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[7]. 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.8. Mailboxes

Mailboxes are used to communicate to 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[8], however it is frequently 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

These properties 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 documentation's entry Main Page → BigWorld (Module) → Member Functions.

6.9. 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.



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

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

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