Creating a Server Plug-in
From Multiverse
| Multiverse Server Plug-ins |
|
Understanding the Servers Processes, Plug-ins, and Agents Creating a Server Plug-in • Defining Command Handlers • Setting Perceiver Radius • |
| Examples and Tutorials |
|
Basic Server Plugin • Faction Plug-in • Creating a Slash Command |
| Server Development |
| Reference |
Contents |
Overview
You can create server plug-ins either in Java or Python. During development, write plug-ins in Python. When your game is ready for production deployment (i.e. finaly public release), you may be able to realize performance gains by moving to Java.
You may not need to use Java at all if performance is not an issue, for example if you don't have a large number of simultaneous players. Whether Java provides significant performance benefit also depends on what your plug-in does. In theory, Python will always be slower because it is interpreted, but in practice the difference may not be significant.
Note: the documentation currently focuses on writing server code in Python. We will provide more documentation on developing plug-ins in Java in the future.
Python scripts can access any Java method defined in a server module. For example, multiverse/config/sampleworld/proxy.py provides examples of using Python
to modify the behavior of the proxy server.
Message processing
A server plugin:
- Sends out messages to other plugins
- Subscribes to and process messages from other plugins
- Associates incoming message to classes that implement the
multiverse.server.engine.Hookinterface, which handle the message processing - Sends messages indirectly to clients.
This base functionality together with the set of pre-built Multiverse MARS plug-ins enable you to create most features you need for your game.
Creating a plugin instance
Create a server plug-in with the EnginePlugin
constructor, passing the string name that will be used to refer to the plugin.
Then you must register the plug-in for it to interoperate with the Multiverse platform and other plug-ins. Having the plug-in registry enables you to turn plugins on and off at a high level. Use the following code to register a plug-in:
myplugin = EnginePlugin("mytestplugin")
Engine.registerPlugin(myplugin)
You can place this code in the config/<world>/extensions_combat.py file (create it if it doesn't exist). This file will be run by the multiverse.sh (or multiverse.bat) file at startup time. The extensions_proxy.py file is not a great candidate since future versions of the server will likely run multiple copies of this plugin and therefore instantiate your plugin multiple times.
You can access other plug-ins local to your process by calling
EnginePlugin.getPlugin(pluginName), which returns
the engine plug-in instance whose name is pluginName.
This is useful mostly in the context of your process's scripts.
You can extend the EnginePlugin class to add
custom methods and fields, however, this is usually unnecessary.
A plug-in on its own can't accomplish much. It needs to use the messaging system to process messages, call into other built-in APIs, and send out its own messages. An example later in this document shows to subscribe to messages and send out messages.
Running the plug-in in a separate process
You can either run your plug-in as part of one of the existing server processes, or run it in its own process.
If your plug-in is relatively simple, and works closely with one of the other existing plug-ins (say, for example, the combat plug-in or mob server plug-in), then you may want to run it in the same process as that plug-in. To run it in an existing process, you write all your code in Python, and add your code to existing Python files, or ones that the servers read by default.
If your plug-in is significantly complex and does not have close relationship to any other plug-ins, you may want to run it in its own process. Doing so may also be simplier to debug your plug-in, since it will have its own log file.
To create your own process, start a Java VM similarly to the server start scripts. For example, if you are using multiverse.sh, add code similar to this:
if [ $verbose -gt 0 ]; then
echo -en "Starting myplugin: \t"
fi
java \
${JAVA_FLAGS} \
-Dmultiverse.loggername=myplugin \
multiverse.server.marshalling.Trampoline \
multiverse.server.engine.Engine \
-i "${MV_BIN}"/mobserver_local.py \
-i "${MV_COMMON}"/mvmessages.py \
-i "${MV_WORLD}"/worldmessages.py \
-m "${MV_COMMON}"/mvmarshallers.txt \
-m "${MV_WORLD}"/worldmarshallers.txt \
"${MV_COMMON}"/global_props.py \
"${MV_WORLD}"/global_props.py \
"${MV_WORLD}"/myscript.py \
&
echo $! > ${MV_RUN}/myplugin.pid
if [ $verbose -gt 0 ]; then
check_process "myplugin" $(cat "${MV_RUN}"/myplugin.pid)
fi
Replace myplugin with the name of your plug-in. Your plug-in may need to read a different set of scripts; this is just an example.
Adding the message agent
To run a plug-in in a separate process, you must add it to the list of agents in the startup script. Modify this line in multiverse.sh:
PLUGIN_NAMES="-a combat -a wmgr1 -a mobserver -a objmgr -a worldreader -a login_manager -a startup -a proxy"
Add -a myplugin to this list, where myplugin is the name (as above) of your server plug-in.
Similarly, if you are using start-multiverse.bat, add your plug-in name to the following line:
set PLUGIN_NAMES=-a combat -a wmgr1 -a mobserver -a objmgr -a worldreader -a login_manager -a startup -a proxy
Multiverse message system
The Multiverse message system provides generic behavior that makes it easy to use existing messages for diverse purposes. The base message class is multiverse.msgsys.Message. The class constructor takes an instance of a MessageType, which in turn is defined by a string message type. The message type identifies the message to the Multiverse
messaging system. Create new message type instances with:
MessageType.intern(message_type_string)
Since you can set the message type property to any string, you can create new message types without defining a new Java class.
Use the setMsgType() method to set the "msgType" property, for example:
public static MessageType MSG_TYPE_FOO = MessageType.intern("wrldMgr_foo");
msg.setMsgType(MSG_TYPE_FOO);
MessageType has many sub-classes. One of the most useful is GenericMessage, which has a property mechanism that enables the caller to create and retrieve arbitrary named message properties.
Property names are strings; property values are any Java object that
implements the Serializable interface. Use the setProperty() and
getProperty() methods to set and get property values. Because the property mechanism is so
powerful, you may not need to create any message classes of your own, but use GenericMessage
instead.
Use the setProperty() method to set a message object property value as follows:
msg = GenericMessage() msg.setProperty(property_name_string, property_value);
where property_name is a string identifying the name of the property and property_value is the value to which it is set.
Use the getProperty method to retrieve a message object property:
property_value = msg.getProperty(property_name);
This method returns a Java object that implements java.io.Serializable.
See also Multiverse Messaging System.
Message filters and subscriptions
A message filter is defined by an object that is a subclass of type multiverse.msgsys.Filter.
This abstract class specifies three methods that any subclass must implement:
-
boolean matchMessageType(Collection<MessageType> messageTypes), which returns true if there is overlap between the message types of the filter and those of the argument, and false if they don't overlap. -
boolean matchRemaining(Message msg), which returns boolean true if theMessageargument is matched by the filter, or false otherwise. -
Collection<MessageType> getMessageTypes(), which returns the collection of message types to which the filter responds.
There are a number of pre-defined message filter classes. The most useful one is
MessageTypeFilter; its matchMessageType method returns true if
the msgType data member of the message is one of those in the collection of message types in the filter.
Creating a subscription
A server plug-in's subscriptions determine which messages it "listens for." Each server plug-in corresponds to a message agent; to create a subscription call one of the createSubscription() methods on Agent, for example:
Engine.getAgent().createSubscription(filter, plugin_instance, flags)
Where:
- filter is the
Filterobject (sub-class) - plugin_instance is the <class="EnginePlugin">multiverse.server.engine.EnginePlugin</class> class for the plug-in.
- The
flagsargument is zero if the filter's messages do not require a response, andMessageAgent.RESPONDERif one or more of the messages requires a response.
For example, the following Python code creates a subscription that deliverers messages of several different types ("foo", "bar" and "baz"):
myFilter = MessageTypeFilter()
myFilter.addType("foo")
myFilter.addType("bar")
myFilter.addType("baz")
Engine.getAgent().createSubscription(my_plugin, myFilter)
Can MessageTypeFilter.addType() actually accept a String arg? It looks like it requires MessageType.
If a plug-in is only listens for one message type, and thus filters
only on the "msgType" property, you don't have to
worry about the details of filters and subscriptions, because there is
another overloading of createSubscription() whose arguments
are the Hook object, described below, and the message
type for messages to be processed by the Hook object:
myPlugin.createSubscription(my_hook_object, my_message_type);
Hook object
The multiverse.server.engine.Hook object supports message processing for one or more
message types. Hook objects form a chain: If a Hook does not process a message, it passes it on to the next Hook in the chain. Once a
message is handled by a hook, subsequent hooks ignore the message.
The hook object has one interesting method:
boolean processMessage(Message msg, int flags);
The flags argument contains information about whether the message requires a response message, and a few other more obscure
pieces of information. In almost all the cases, the flags are ignored.
A hook's processMessage() method returns false if the hook has
handled the message, and true otherwise (that is, if it should be handled by subsequent hooks in the hook chain).
Each EnginePlugin has a hook manager. To get the
hook manager, call the engine.getHookManager() method.
Then, you can add hooks by calling hookManager.addHook(message_type, hook).
However, in the simplest case of using the two-argument overloading of
createSubscription, you won't need to bother with the
hook manager.
Steps to create an engine plugin
The standard server plugin processes messages of a certain type. To create this kind of engine plugin:
- Define the format of messages to be processed by your plugin. Most of the time, you can use the existing
Messageclass, with a particular "msgType" property that identifies it, and one or more addditional properties representing the message payload. - Write the function to be called to process messages of the type identified by your subscription. This is the heart of your engine plugin; everything else in the usual case is boilerplate. If you're writing in Java, the function must implement the
Hookinterface; if you're writing in Python, the function must be a member function of a new class, whose base class isHook. - Create your
EnginePlugininstance. In the usual case that all the work is to be done in the hook object, all that is required is to create an instance of the existing classEnginePlugin, rather than creating a derived class fromEnginePlugin. Call theEnginePluginconstructor, passing a string to serve as the name of the plugin instance. Save the newly-created instance so you can refer to it below. - Invoke the static method
Engine.registerPlugin, passing the new engine plugin instance. - Call the two-argument overloading of the engine plugin method
createSubscription
That's all there is to it! The only part that requires some creativity is the function that processes messages delivered by your subscription.
A complete example
| Contributed by: Multiverse | Last Tested: 01 Nov 07 | Tested By: David Stryker | Tested With: 1.1 |
Suppose you wish to create a server plugin that fetches and stores
data from an external source. Assume that requests are
Message instances whose "msgType" is "fetchstore", with
the following additional named properties:
- operation: "fetch" or "store"
- data_location: a string saying where to get or save the data, interpreted by the storage layer invoked by the processMessage method of your message hook.
- data: This property is only present for "store" requests, and represents Java serializable value that will be stored.
For the "store" operation, processing ends when the data is in stable storage. For the "fetch" operation, the data must be returned to the caller. This is done by creating a new message, whose "msgType" is "FetchResult", with the properties data and data_location. Some other engine plugin subscription will be listening for this "fetchresult" message.
Below is a Python implementation of the fetch/store engine plugin, using the file system as the "database" from which values are fetched and stored. It could have been written in Java as well, of course.
from multiverse.msgsys import *
from multiverse.server.engine import *
from multiverse.server.util import *
from multiverse.msgsys import *
# Define the hook interface class
class FetchStoreHandler(Hook):
# The function that processes fetch/store packets
def processMessage(self, msg, flags):
directory = "C:/Junk/" # Fill in your favorite directory
Log.debug("myPluginExample: Received a message " + str(msg))
operation = msg.getProperty("operation")
# It's a GenericMessage, so we can fetch and set properties
data_location = msg.getProperty("data_location")
if operation == "store":
data = str(msg.getProperty("data"))
Log.debug("myPluginExample: Received store to " + data_location + "; data is " + data)
FILE = open(directory + data_location,"w")
FILE.write(data)
FILE.close()
elif operation == "fetch":
FILE = open(directory + data_location,"r")
data = FILE.read()
FILE.close()
Log.debug("myPluginExample: Received fetch from " + data_location + "; data is " + data +
"; sending response to caller")
Engine.getAgent().sendStringResponse(msg, data)
# Create the MessageType
msgType_fetchstore = MessageType.intern("fetchstore")
# Add it to the list of messages sent by this process
Engine.getAgent().addAdvertisement(msgType_fetchstore)
# Create the engine plugin instance
myPlugin = EnginePlugin("myPlugin")
Engine.registerPlugin(myPlugin)
myPlugin.createSubscription(FetchStoreHandler(), msgType_fetchstore, MessageAgent.RESPONDER)
# Test the new plugin by sending a couple of "fetchstore" messages
# Create and send a "fetchstore" message with operation "store"
myMsg = GenericMessage()
myMsg.setMsgType(msgType_fetchstore)
myMsg.setProperty("operation", "store")
myMsg.setProperty("data", "data1")
myMsg.setProperty("data_location", "value1")
Engine.getAgent().sendBroadcast(myMsg)
Log.debug("myPluginExample: Sent store operation to location value1 for value data1")
# Create and send a "fetchstore" message with operation "fetch"
myMsg = GenericMessage()
myMsg.setMsgType(msgType_fetchstore)
myMsg.setProperty("operation", "fetch")
myMsg.setProperty("data_location", "value1")
data = Engine.getAgent().sendRPCReturnString(myMsg)
Log.debug("myPluginExample: Sent fetch operation from location value1, got back " + data)
Running the example
To run the example, add it to one of the world-specific configuration files in
/config/world_name/; for example at it at the top of
multiverse/config/sampleworld/extensions_proxy.py to add the example plugin to the
proxy processs. After doing so, start up sampleworld, and you will see the following
lines in your /logs/sampleworld/proxy.out log file, interspersed with other lines:
DEBUG [2007-11-01 12:06:20,765] main myPluginExample: Sent store operation to location value1 for value data1 ... DEBUG [2007-11-01 12:06:20,765] pool-6-thread-3 myPluginExample: Received a message multiverse.msgsys.GenericMessage@1fd8905 ... DEBUG [2007-11-01 12:06:20,765] pool-6-thread-3 myPluginExample: Received fetch from value1; data is data1; sending response to caller ... DEBUG [2007-11-01 12:06:20,921] main myPluginExample: Sent fetch operation from location value1, got back data1
Client-server messaging
In addition to sending messages between server plug-ins, you can also send messages between the client and the servers.
See also Using Extension Messages.
Sending messages from server to the client
It is often neccessary to send messages from the server to the client, for example to update a player's skill level, or to make the client play an animation or change the skybox. Implementing a message consists of two parts:
- Defining the message on the server and sending it to the client.
- Defining a handler for the message on the client.
The Multiverse platform comes with a set of predefined messages along with corresponding default client handlers; see Multiverse Message Catalog.
This section describes a simple example message containing a text string and pass it to a specific user. When the client receives the string, it will prints in in the multiverse client log and to the user's screen.
Defining a message on the Server
This example assumes that you have looked for a user and you know the player you want to send it to. In this example, it was player id 11101. This python method was called from another program that looked through the list of user's then called this method passing it an array of logged in users (pOid). In most situations, you can use the ExtensionMessage class to send a message to the client that encapsulates a set of string key/value pairs.
It uses the WorldManagerClient.TargetedExtensionMessage message passing it the player id. You pass a set of key/value pairs of which the most important one is the "ext_msg_subtype" pair. This is where you identify the sub-message name. You can think of it much like the message name in the server example, but, you don't advertise it like you needed to on the server. There is one basic message type between the server and the client and the sub-messages as defined by the property "ext_msg_subtype", makes each message unique. In this example, I named the sub-message "myString". I then further set properties to pass the data associated with the message. The message had three keys myString.printstring, myString.channel and myString.Oid with the values "Hi Mojo", 5" and the player id as a string. It then uses the sendBroadcast method to send the message.
def sendUserMsg(pOid):
print 'Got PlayerID', pOid
player = pOid[0]
print 'Player is ', player
if player == 11101:
print 'in message processing'
msg = WorldManagerClient.TargetedExtensionMessage(player)
print 'after msg'
msg.setProperty("ext_msg_subtype","myString")
msg.setProperty("myString.printstring", "Hi Mojo")
msg.setProperty("myString.channel", "5")
msg.setProperty("myString.Oid", str(player))
Engine.getAgent().sendBroadcast(msg)
print 'after send broadcast'
Defining the message handler on the Client
To create a message handler on the client, add code to your asset repository in the following file: Interface/FrameXML/Library.py.
Once downloaded to the client, it will be in
Client_Home\Worlds\worldname\Interface\FrameXML\Library.py.
After creating the message handler, you must register it for the appropriate type of ext_msg_subtype sub-messages. Doing this tells the client to send some ExtensionMessage messages to this message handler. Notice how the ext_msg_type has been replaced by the ext_msg_subtype in the Message Handler in MV 1.1. Also, you use the props dict object instead of the message object to get the properties associated with the sub-message.
Add the following lines to Library.py. You can edit the sampleworld's version locally if you want:
def HandleTestExtensionMessage(props):
ClientAPI.Log("In HandleExtensionMessage")
ClientAPI.Log(str(props["myString.printstring"]))
ClientAPI.Log(str(props["myString.channel"]))
if not props.ContainsKey('ext_msg_subtype'):
return
if props['ext_msg_subtype'] != "myString":
return
ClientAPI.Log("Got extension message from %s" % str(props["myString.Oid"]))
ClientAPI.Write("Got extension message from %s" % str(props["myString.Oid"]))
ClientAPI.Network.RegisterExtensionMessageHandler("myString", HandleTestExtensionMessage)
Here is the result in the MultiverseClient.log in the client world log file.
INFO [2008-02-11 19:35:48,919] MessageDispatcher Handle message called for Multiverse.Network.ExtensionMessage INFO [2008-02-11 19:35:48,919] ClientAPI In HandleExtensionMessage INFO [2008-02-11 19:35:48,919] ClientAPI Hi Mojo INFO [2008-02-11 19:35:48,919] ClientAPI 5 INFO [2008-02-11 19:35:48,919] ClientAPI Got extension message from 11101
Transferring data from server to client
On the server side, put your data in object properties, and you can get to it from the client.
The Sampleworld quest marker script and the animation script are good examples.
The quest marker script is in /Scripts/QuestMarkers.py in the Sampleworld asset repository.
import ClientAPI
questMarkers = {}
def RemoveMarker(worldObj):
# if there is a quest marker, remove it from the object
# ClientAPI.Write("QuestMarkers:RemoveMarker: Name = " + worldObj.Name + ", OID = " + str(worldObj.OID))
marker = questMarkers[worldObj.OID]
if not marker is None:
# ClientAPI.Write("QuestMarkers:RemoveMarker: Removing " + marker.Name)
worldObj.DetachObject(marker)
marker.Dispose()
questMarkers[worldObj.OID] = None
def AddMarker(worldObj, markerMeshName):
ClientAPI.Write("QuestMarkers:AddMarker: Name = " + worldObj.Name + ",
OID = " + str(worldObj.OID) + ", mesh = " + markerMeshName)
marker = questMarkers[worldObj.OID]
if not marker is None:
ClientAPI.Write("QuestMarkers: attempt to add a marker when one is already present")
marker = ClientAPI.Model.Model("marker." + str(worldObj.OID), markerMeshName)
worldObj.AttachObject("questavailable", marker)
questMarkers[worldObj.OID] = marker
def PropertyChangeHandler(worldObj, propName):
if propName == "questavailable" or propName == "questconcludable":
questAvailable = worldObj.CheckBooleanProperty('questavailable')
questConcludable = worldObj.CheckBooleanProperty('questconcludable')
# remove any previous marker
RemoveMarker(worldObj)
# if we need a marker now, add it
if questConcludable:
AddMarker(worldObj, "question.mesh")
elif questAvailable:
AddMarker(worldObj, "exclamation.mesh")
# This function is an event handler that runs when the world has been initialized.
def ObjectAddedHandler(worldObj):
if worldObj.ObjectType == ClientAPI.WorldObject.WorldObjectType.Npc:
# ClientAPI.Write("QuestMarkers: added npc object")
questMarkers[worldObj.OID] = None
# register event handlers for object
worldObj.RegisterEventHandler('PropertyChange', PropertyChangeHandler)
# ClientAPI.Write("QuestMarkers: Registered npc event handlers")
def ObjectRemovedHandler(worldObj):
if worldObj.ObjectType == ClientAPI.WorldObject.WorldObjectType.Npc:
#ClientAPI.Write("QuestMarkers: removing object")
# Remove the marker from the object
RemoveMarker(worldObj)
# now remove the marker from our dictionary
del questMarkers[worldObj.OID]
# remove event handlers for object
worldObj.RemoveEventHandler('PropertyChange', PropertyChangeHandler)
# Register an event handler that will run when the world has been initialized.
ClientAPI.RegisterEventHandler('ObjectAdded', ObjectAddedHandler)
ClientAPI.RegisterEventHandler('ObjectRemoved', ObjectRemovedHandler)
Client animation script
This is the example script /Scripts/ClientAnimations.py in the Sampleworld asset repository.
TBD
