Multiverse Object Architecture
From Multiverse
| Multiverse Servers |
|
Installing • Installing on Linux • Running • Troubleshooting • FAQ • Release Notes • Updating • JMX Monitoring & Mgmt. |
| Infrastructure |
|
Platform Architecture • Registering a World • Proxy Server • Event Handling • World Manager • Voice Server |
| Messaging System |
|
Perception Messaging • Using Extension Messages • Message Marshalling • Multi-subject Messaging • Message Catalog |
| Object Architecture |
|
World Instancing • Server Object Search • Server Regions • Server Markers |
| Scalability and Performance |
| Reference |
|
File Layout • Property File • Logging • API |
NOTE: The Multiverse object architecture was previously referred to as the distributed component model.
Contents |
Overview
The Multiverse object architecture provides scalability, improved reliability, and a persistence mechanism. Although the architecture adds some complexity to object scripting, the payoff is increased reliability and true scalability, so your game can successfully handle hundreds, thousands, and ultimately even hundreds of thousands of players.
In a non-distributed architecture, a single object server manages all objects. Not only is such an architecture not scalable, but it also presents a single point of failure: if the object server goes down, then all object operations would fail, which would essentially halt most activity in a gameworld. In the Multiverse distributed architecture, there can be many object servers, providing virtually unlimited scalability. If one of the servers goes down, only the objects that it manages are affected, which improves the reliability of the system.
Before describing the object model, it's important to understand what an "object" is in this context. Server-side objects can represent both physical and non-physical objects, for example, players, mobs and NPCs, items, and structures. Additionally, light sources and sounds can also be objects. Basically, any distinct thing that may need to be persistent is a good candidate for an object. For example, a sword is an object, because it can exist, either on its own, or in a player's inventory. However, player health is not an object since it has no meaning on its own: it is intrinsically tied to the player. Thus, it is better represented as a property/statistic of a player.
Object operations
The main operations you can perform on an object are:
- Creating it initially (also called generation).
- Loading it from the database after it has been saved.
- Unloading
- Deleting it.
- Saving it to the database (persistence)
Once you have loaded an object, you can inspect and modify it.
Object lifecycle
Multiverse objects can go through the following generalized lifecycle:
- Generation/creation: Performed once per object. Object is in "loaded" state after generation.
- Saving/persistence: Object changes are automatically saved at regular intervals. Objects may be explicitly saved by function call.
- Loading: Previously saved objects can be loaded after server restart or after they've been unloaded.
- Unloading: Unloaded objects are despawned and removed from memory. If the object is persistent, they can be subsequently loaded. Unloading a non-persistent object is equivalent to deleting it.
- Deletion: Deleted objects are despawned, removed from memory and the database.
This lifecycle only applies to objects generated through ObjectManagerClient.generateObject().
The lifecycle does not apply to objects created locally by creating instances of Entity and MVObject objects.
Creating objects
Creating an object requires:
- Defining an object template.
- Adding any desired template overrides.
- Registering the template to the object manager's repository.
- Generating the object using the template.
- Optionally spawning the object.
Defining objects
Use an object template to define an object. A template basically stores
name/value pairs. The names are organized in a namespace to identify the plug-in server to which the data is relevant. This provides the cross-server distribution, as previously
described. For example, the following is a simple example template which doesn't actually work. A later, more complex, example is a working example.
Each call to the put() method includes
the namespace as the first argument:
zombieTempl = Template("zombie_template")
zombieTempl.put(InventoryClient.NAMESPACE, "slot1", "iron gloves:equipped_true")
zombieTempl.put(InventoryClient.NAMESPACE, "slot2", "steel shortword:equipped_false")
zombieTempl.put(CombatClient.NAMESPACE, "health", 20)
This example code refers to two namespaces:
InventoryClient.NAMESPACE for inventory "bags", and CombatPlugin.NAMESPACE for combat-related data.
When you create an object from this template, the object manager plug-in
sends each plug-in server the template data for the appropriate namespace so
it can create a sub-object.
It's up the plug-in writer (you) to determine what the values actually mean. Sometimes the values are composites, such as the inventory slots. In this case, the colon character ":" is used as a separator, and the value after it indicates whether the zombie starts out with that item equipped. It is up to the plug-in to parse such composite values.
After defining a template, you must register it before any object can be created with it. This is because the ObjectManagerClient.generateObject() call takes in the template name as a parameter. A plug-in dynamically adds a template to the object manager's repository by calling:
ObjectManagerClient.registerTemplate(zombieTempl)
As a convenience, the server will look in your world config file for a templates.py file (multiverse/config/<world>/templates.py.) You can add templates there and the object manager plugin will load them at startup time.
Template overrides
Templates usually do not define everything you need to generate an object. For example, you may want to set a name for the object and its location on creation time. You may also want to override certain values in a template (such as making an orc stronger than the default orc.) This is done by supplying an override template to the ObjectManagerClient.generateObject() method. An override template is a Template object and all values are added to the first template passed in. Any conflicting key/values use the override template value instead.
zombieOverride = Template() zombieOverride.put(WorldManagerClient.NAMESPACE, WorldManagerClient.TEMPL_LOC, Point(1000,0,0)) # set the location of the zombie zombieOverride.put(CombatClient.NAMESPACE, "health", 40) # this zombie is twice as healthy! zombieOverride.put(CombatClient.NAMESPACE, "speed", 200) # supply a new (optional) speed parameter
Generating objects
After defining and registering the template, you can generate the object as follows:
oid = ObjectManagerClient.generateObject("zombie_template", zombieOverride)
This invocation sends a message to the ObjectManagerPlugin requesting creation of an object based on a template. The first parameter, "zombie_template," is the name supplied previously in the template constructor. The second parameter is the override template just defined.
The object manager plug-in will look up the template, create a master object with its own unique OID, and request sub-objects to be created for each namespace in the template. Finally, it returns the master object OID to the caller. All plugins use this same OID, together with the plugin namespace(s), to represent per-plugin data. All plugins should listen for messages based on the master OID. See the section on plugin development for more information.
Spawning objects
Some objects needs to be spawned. These are objects in the world that others (players and NPCs) can see. To spawn an object, call WorldManagerClient.spawn() with the master object OID, returned by ObjectManagerClient.generateObject().
WorldManagerClient.spawn(oid)
This method call makes the object visible to other aspects of the system, so, for example, other players can see the object and interact with it.
Creating objects dynamically
You may want to spawn a building or some object dynamically by the server rather than from the World Editor. Here is an example where you don't even have a template you are using, all properties are specified in the override template. The ObjectManagerClient.BASE_TEMPLATE is actually an empty template that is useful as a starting point. This is an actual example:
# create the override template. we are assuming the dc, loc, etc, objects are initialized
Template overrideTemplate = new Template();
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_OBJECT_TYPE, "STRUCTURE");
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_DISPLAY_CONTEXT, dc);
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_LOC, loc);
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_ORIENT, orient);
# scale could be replaced with MVVector(0.75, 0.75, 0.75)
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_SCALE, scale);
overrideTemplate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_PERCEPTION_RADIUS, perceptionRadius);
# generate the object here
Long objOid = ObjectManagerClient.generateObject(ObjectManagerClient.BASE_TEMPLATE,
overrideTemplate);
# spawn the object - so that players can see it
WorldManagerClient.spawn(objOid);
The following is the same basic code from a Python perspective.
In Templates.py:
dc = DisplayContext("wrighthouse3.mesh")
item = Template("genhouse_template")
item.put(WorldManagerClient.NAMESPACE, WorldManagerClient.TEMPL_DISPLAY_CONTEXT, dc)
ObjectManagerClient.registerTemplate(item)
In your Python module for creating dynamic content:
from multiverse.mars import *
from multiverse.mars.objects import *
from multiverse.mars.core import *
from multiverse.mars.events import *
from multiverse.mars.util import *
from multiverse.mars.plugins import *
from multiverse.server.plugins import *
from multiverse.server.math import *
from multiverse.server.events import *
from multiverse.server.objects import *
from java.lang import *
from java.util import *
from multiverse.server.engine import *
from multiverse.server.util import *
from multiverse.server.plugins import *
from multiverse.msgsys import *
#Create the engine plug in instance
createObjectplugin = EnginePlugin("createObjectplugin")
Engine.registerPlugin(createObjectplugin)
# create message type
msgType_Generate_Object = MessageType.intern("mv.GENERATE_OBJECT")
Engine.getAgent().addAdvertisement(msgType_Generate_Object)
#Spawn Object
houseOverride = Template()
loc = Point(7124562, 122881, -1934532)
scale = MVVector(10, 10, 10)
orient = Quaternion(0, 0, 0, 1)
perceptionRadius = Integer(0)
houseOverride.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_OBJECT_TYPE,
WorldManagerClient.TEMPL_OBJECT_TYPE_STRUCTURE)
houseOverride.put(WorldManagerClient.NAMESPACE, WorldManagerClient.TEMPL_LOC, loc)
houseOverride.put(WorldManagerClient.NAMESPACE, WorldManagerClient.TEMPL_ORIENT, orient)
houseOverride.put(WorldManagerClient.NAMESPACE, WorldManagerClient.TEMPL_SCALE, scale)
houseOverride.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_PERCEPTION_RADIUS,
perceptionRadius)
oid = ObjectManagerClient.generateObject("genhouse_template", houseOverride)
WorldManagerClient.spawn(oid)
Creating characters
Character generation also uses the object architecture. The script in mv-home/config/common/character_factory.py defines a CharacterFactory called to generate a new character. The character_factory.py script is run at start-up time.
This example includes APIs from version 1.5
# create a display context for how the base mesh looks
displayContext = DisplayContext("human_female.mesh")
displayContext.addSubmesh(DisplayContext.Submesh("bodyShape-lib.0",
"human_female.skin_material"))
displayContext.addSubmesh(DisplayContext.Submesh("head_aShape-lib.0",
"human_female.head_a_material"))
displayContext.addSubmesh(DisplayContext.Submesh("hair_bShape-lib.0",
"human_female.hair_b_material"))
#
# create a default player template, the name will not be the player name
#
player = Template("DefaultPlayer")
# set the display context (mesh)
player.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_DISPLAY_CONTEXT,
displayContext)
# set the type of object it is, in this case its a user which will by
# default have a perceiver
player.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_OBJECT_TYPE,
ObjectTypes.player)
# set the starting animation of this object
player.put(AnimationClient.NAMESPACE,
AnimationClient.TEMPL_ANIM,
MarsAnimation.IDLE.getName())
# set up the invetory, an asterix (*) before the item name means
# it starts out equipped
player.put(InventoryClient.NAMESPACE,
InventoryClient.TEMPL_ITEMS,
"*Leather Tunic; *Leather Pants")
# register the template with the object manager so that
# we can actually use it
ObjectManagerClient.registerTemplate(player)
# now create the character factory which generates an object
# from this template
class SampleFactory (CharacterFactory):
def createCharacter(self, worldName, uid, properties):
name = properties.get("characterName")
# Player start location
loc = Point(-135343, 0, -202945)
# Player start instance; assumes you have an instance named "default"
instanceOid = InstanceClient.getInstanceOid("default")
overrideTempate = Template()
if name:
overrideTempate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_NAME, name)
overrideTempate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_INSTANCE, Long(instanceOid))
overrideTempate.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_LOC, loc)
# Initialize the player's instance restore stack
restorePoint = InstanceRestorePoint("default", loc)
restorePoint.setFallbackFlag(True)
restoreStack = LinkedList()
restoreStack.add(restorePoint)
overrideTempate.put(Namespace.OBJECT_MANAGER,
ObjectManagerClient.TEMPL_INSTANCE_RESTORE_STACK, restoreStack)
overrideTempate.put(Namespace.OBJECT_MANAGER,
ObjectManagerClient.TEMPL_CURRENT_INSTANCE_NAME, "default")
# Make the player persistent (will be saved in database)
overrideTempate.put(Namespace.OBJECT_MANAGER,
ObjectManagerClient.TEMPL_PERSISTENT, Boolean(True))
# Create the player object
objOid = ObjectManagerClient.generateObject("DefaultPlayer",
overrideTempate)
return objOid
sampleFactory = SampleFactory()
# register the character factory
LoginPlugin.getCharacterGenerator().setCharacterFactory(sampleFactory);
Generating sub-objects
You may need to create a new namespace in a template to express a different aspect of an object. The following example adds a "sound" namespace to the templates to represent a sound that an object emits.
The first step is to decide on the new namespace for the templates. It cannot already be used. In this example it is called "sound".
You could just pass the string "sound" to the template's put() method, and that method would create an interned Namespace object to represent the sound namespace. But good practice is to declare a namespace object, and then refer to that object. In the case of the sound namespace, declare it with the following statement:
Namespace soundNAMESPACE = Namespace.intern("sound")
Next you need to decide on the key/values to use. This example requires a "soundfile" key with a string value representing the filename.
Put this together to modify the zombie template (in Python) to add the sound data:
zombieTempl = Template("zombie_template")
zombieTempl.put(InventoryClient.NAMESPACE, "slot1", "iron gloves:equipped_true")
zombieTempl.put(InventoryClient.NAMESPACE, "slot2", "steel shortword:equipped_false")
zombieTempl.put(CombatClient.NAMESPACE, "health", 20)
# here is the new code
zombieTempl.put(soundNAMESPACE, "soundfile", "moan.ogg")
Now that you have a template, you need to write some code to handle the new namespace. You must register your plugin with the namespace in your plugin's onActivate() method. The server calls this method when your plug-in is starting up.
public void onActivate() {
// pass in the namespace and also the hook to call when a sub object is being
// generated
registerPluginNamespace(soundNAMESPACE, new GenerateSubObjectHook());
}
As you see, the above code passes in soundNAMESPACE for the namespace, and a hook for processing sub-object generation. This hook will get called when someone is generating an object that has a
"sound" namespace. The hook is just a Hook object. Here is an example implementation:
class GenerateSubObjectHook implements Hook {
public boolean processMessage(Message m, int flags) {
// these are always GenerateSubObjectMessages
ObjectManagerClient.GenerateSubObjectMessage msg =
(ObjectManagerClient.GenerateSubObjectMessage) m;
// grab the template
Template template = msg.getTemplate();
// get the master oid (the oid that binds us all!)
Long masterOid = msg.getMasterOid();
Log.debug("masterOid=" + masterOid + ", template=" + template);
// lets get our soundfile
String soundFile = template.get("sound", "soundfile");
// generate the subobject
Entity obj = new Entity();
// do whatever else you want with the entity here, such as setting
// properties, etc.
obj.setProperty("soundFile", soundFile);
Log.debug("created entity " + obj);
// Register the entity with entity map. This lets us
// easily find the sub-object associated with our
// namespace this automatically registers the entity with
// the entity map so you can call Entity.getEntity(oid,
// soundNAMESPACE) for your sub object
Entity.addEntity(masterOid, soundNAMESPACE, obj);
// send a response message with the oid back to the object manager
sendSubObjectResponse(msg, masterOid, soundNAMESPACE);
// a more complicated version exists where you depend on other sub objects
// being created before you can finish initializing, see EnginePlugin API for
// more details (it will be written soon)
return true;
}
}
Complex template example
The following code provides a more elaborate example of a wolf template showing many of the most useful template parameters:
wolf = Template("Wolf")
# for the world manager - this sets the object's name. often this is
# set in an override template instead
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_NAME,
"Sample Wolf")
# for the world manager - this sets the object's location on spawn
# this is usually set on the override template since you dont want
# all wolves to appear in the same location
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_LOC,
new Point(1,203,14249))
# the world manager plugin knows how to associate a display context from a template
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_DISPLAY_CONTEXT,
DisplayContext("wolf.mesh"))
# for the world manager - its orientation
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_ORIENT,
Quaternion.fromAngleAxisDegrees(20, new MVVector(0,1,)))
# for the world manager, the object's scale, should almost always be uniform
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_SCALE,
new MVVector(1,1,1))
# for the world manager, mobs can have a perception radius
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_PERCEPTION_RADIUS,
1000000)
# object types are used to determine whether it has a perceiver, you collide with it, etc.
# this can be also be ObjectTypes.player, ObjectTypes.mob, ObjectTypes.structure
# but you can make your own type also
wolf.put(WorldManagerClient.NAMESPACE,
WorldManagerClient.TEMPL_OBJECT_TYPE,
ObjectTypes.mob)
# for the animation plugin, its default animation
wolf.put(AnimationClient.NAMESPACE,
AnimationClient.TEMPL_ANIM,
MarsAnimation.IDLE.getName())
# for the inventory plugin, what items it comes with. the value
# is a semi-colon seperated list of template names.
wolf.put(InventoryClient.NAMESPACE,
InventoryClient.TEMPL_ITEMS,
"rapier; rapier")
ObjectManagerClient.registerTemplate(wolf)
Loading objects
Persistent objects can be loaded after they have been saved. This can happen after an object has been unloaded, or after a server restart. Player objects are loaded during the login process.
Distributed objects are loaded by the object manager. First, the master object is loaded from the database. Then messages are sent to load each sub-object within its plugin. Only after all sub-objects are successfully loaded is the object considered loaded.
ObjectManagerClient.loadObject(oid);
Loading an object does not spawn the object. If the object has a world manager sub-object, then the object must be explicitly spawned after loading:
// Spawn if load was successful if (ObjectManagerClient.loadObject(oid) == oid) WorldManagerClient.spawn(oid);
If an object's instance is not already loaded, then the instance is automatically loaded. If the instance does not exist (was not persistent), then loadObject() fails.
Namespaces
Namespaces partition sub-objects among server plug-ins, so that each plug-in maintains its own state for each object. Namespaces provide a way to distinguish sub-objects when storing their state in the database, since all sub-objects have the same OID.
The servers distinguish objects to be saved based on both the OID and the plug-in's Namespace object, so several plug-ins can store object state using the same OID as long as they use their own namespaces. The master OID sent by the Object Manager is the same OID by which the plug-ins refer to their object state.
The database stores both the namespace strings as well as small integers that provide a shorthand for the namespace strings. In plug-in code, Namespace instances replace the namespace strings. All plug-ins agree on the set of namespaces, because during startup each server process caches the mapping between namespace strings and namespace numbers by reading the mapping from the database.
Namespace objects are identified by a string, that the server uses to store the Namespace objects in the database.
When the plug-in starts, it loads the Namespace objects from the database.
An Entity, which represents a particular plugin's sub-object, "knows" what Namespace it belongs to. So, for example, a combat sub-object stores combat-related data such as health and mana, a location sub-object stores geographical data such as the location, current movement direction and speed, and so on. The combat plugin server will keep the combat sub-object locally, but can also make network queries to get data from sub-objects stored on other plug-ins, if necessary.
Creating a namespace
To create a Namespace object "on the fly," call the static method Namespace.intern(). If the string is found, it returns the associated namespace object; otherwise, it stores the
new namespace in the database, and creates and returns the Namespace object.
For example, to add a namespace for plugin XYZ:
First, add the following to XYZClient.java:
public static Namespace NAMESPACE = null;
Then add the following to multiverse/config/myworld/global_props.py:
from multiverse.mars.plugins import *
from multiverse.server.engine import *
XYZClient.NAMESPACE = Namespace.intern("MYWORLD.xyz")
You might need additional imports to access your XYZClient.
Finally, compile and restart your server.
NOTE: You might get errors about duplicate namespaces. These only occur the first time the server is run after updating global_props.py and are harmless (assuming namespace creation is the last line of global_props.py).
Predefined namespaces
There are a number of predefined namespaces, used by the predefined server plug-ins. Of course, you can define your own namespaces for your own plug-ins.
The following table describes the predefined namespaces.
| Plug-in | Namespace String | Description |
|---|---|---|
| All | Namespace.TRANSIENT
| Contains objects that are never stored in the database. |
| Object manager | Namespace.OBJECT_MANAGER
| Enables the ObjectManager to keep track of the other namespaces that make up a conceptual object. |
| World manager | Namespace.WORLD_MANAGER
| Contains object location, direction and orientation for use by the world manager. |
| Combat plug-in | Namespace.COMBAT
| Contains combat state, such as combat statistics, health, mana, etc. |
| Mob plug-in | Namespace.MOB
| Contains ObjectStub entities for mobs.
|
| Inventory plug-in | Namespace.BAG
| Contains inventory Bag objects.
|
| Inventory plug-in | Namespace.MARSITEM
| Contains inventory multiverse.mars.objects.MarsItem objects that are saved in Bags.
|
| Quest plug-in | Namespace.QUEST
| Contains quest objects and the quest states for players. |
Getting object property values set in World Editor
In World Editor, you can add name/value properties to objects. There are two ways to get the values of these properties in server code:
- Synchronous: Use
EnginePlugin.getObjectProperties()This technique requires knowing the property names and the namespace they're in. You need one call per namespace. - Asynchronous: Use
WorldManagerClient.updateObject(). This (poorly named) method requests all namespaces to respond with property messages about the second argument, updateOid. The namespaces each send aTargetedPropertyMessageto the notifyOid (the firs argument). The proxy server uses this technique when a player perceives a new object, using the player OID for the notifyOid. To capture these property messages, subscribe to them, for example with a PerceptionFilter that enables you to subscribe to multiple OIDs. See Perception Messaging for a good example of this usage. AddWorldManagerClient.MSG_TYPE_TARGETED_PROPERTYto the filter. You can use any OID for the notifyOid, as long as it is unique to your plug-in. It doesn't even have to be associated with an object. You can get an OID withEngine.getOIDManager().
Properties set in the world editor are available from the world manager name space, Namespace.WORLD_MANAGER.
Getting and setting object properties
The multiverse.server.engine.EnginePlugin class provides a rich set of static methods for getting and setting properties of sub-objects. Calling an object property method sends a message to the plug-in that maintains the property or properties in question.
You can get or set several properties with a single method call. Since getting or setting an object property requires a network round trip, and you often want to get or set several properties at a time, this ability represent a significant economy.
When getting or setting multiple properties, you have the option of passing in a collection type containing all the properties, or using a varargs method in which each key or value is passed as a separate argument. Using the varargs approach requires less code, because instead of creating a List or Set object, filling it, and then calling the method, you just pass the required keys and/or values in the method call.
In the case of setting properties, there are separate methods to wait for a response from the set operations, which is a message containing the previous values of the properties; or alternatively to initiate the set operation and continue without waiting for a response. In the case of setting object properties, even if you don't care about the return value itself, you might need to know that the set operation has happened before proceeding, so in those cases you'll need to call the method that waits for a return value.
Use the same methods for both transient and persistent properties. Rather than distinguishing them by having different get and set methods, they are distinguished by declaring the keys for transient properties. To declare that a key and the corresponding value transient, register the transient key by calling the static method Entity.registerTransientPropertyKey, passing the key to be declared transient as the argument.
All the methods take a Namespace argument that routes the property message to the appropriate server plug-in, since different plug-ins maintain different sets of properties in their sub-objects as well as an OID argument that is the object ID of the relevant object.
There are different sets of methods for getting and setting properties that consist of a single object and properties that consist of multiple objects.
Single-object property
- Getter:
getObjectProperty()- returns the value of the object property associated with the given key, or null if there is none. - Setter that waits for response:
setObjectProperty()- sets the object property identified by the key/value pair, and waits for a response. The return value is the previous value associated with the key. - Setter that does not wait for response:
setObjectPropertyNoResponse()- sets the object property identified by the key/value pair.
Multiple-object property
Getters
- Keys in a list:
getObjectProperties()- gets a list of the values of the object properties associated with the given list of keys. The return value is a list of the values associated with the keys. - Keys In varargs:
getObjectProperties()- returns aListof the values of the object properties associated with the list of keys supplied as varargs.
Setters
- Keys/values in a property map:
-
setObjectProperties()- sets the object properties identified by the key/value pair in the property map argument. Waits for a response, and then returns aListof the previous values associated with the keys. -
setObjectPropertiesNoResponse()- sets the object properties identified by the key/value pair in the property map argument. Does not wait for a response.
-
- Keys/values in varargs:
-
setObjectProperties()- Sets the object properties identified by the alternating key and value arguments supplied as varargs. Waits for a response, and then returns aListof the previous values associated with the keys. -
setObjectPropertiesNoResponse()- Sets the object properties identified by the alternating key and value arguments supplied as varargs. Does not wait for a response.
-
Persistence
Separate APIs are available for persisting distributed and local objects.
Distributed objects are those created with ObjectManagerClient.generateObject(). Distributed objects have one or more sub-objects identified by namespaces and implemented by plugins.
Local objects are created via "new Entity()" or "new MVObject()". They are not known to the object manager.
Entities and MVObjects are used to implement distributed object sub-objects. In this context, they are not local objects, but aspects of a distributed object.
"Dirty" objects
Object changes can be saved automatically by marking them "dirty." The PersistenceManager class periodically saves dirty objects. Both sub-objects and local objects can use the PersistenceManager. However local objects must have a non-transient namespace.
A dirty sub-object only saves itself, not the other sub-objects.
Distributed object persistence
Distributed objects have a persistence flag indicating whether they are saved to the database. Normally this flag applies to all sub-objects, but that is not a strict requirement.
Objects created via ObjectManagerClient.generateObject() are not persistent by default. Two mechanisms are available to enable persistence:
- Set property
ObjectManagerClient.TEMPL_PERSISTENTto true in the object Template prior to object generation - Call
ObjectManagerClient.setPersistenceFlag(oid,true)on a loaded object
Calling ObjectManagerClient.saveObject(oid) or PersistenceManager.setDirty(true) does nothing on a non-persistent object.
Enabling persistence on new objects
To enable the persistence flag on a new distributed object, set ObjectManagerClient.TEMPL_PERSISTENT to true in the OBJECT_MANAGER namespace.
overrideTemplate = Template()
overrideTemplate.put(Namespace.OBJECT_MANAGER,
ObjectManagerClient.TEMPL_PERSISTENT, Boolean(True))
objOid = ObjectManagerClient.generateObject("MyTemplate", overrideTemplate)
Enabling persistence on existing objects
To enable the persistence flag on an existing distributed object, call ObjectManagerClient.setPersistenceFlag(oid,true) . This function only works on loaded objects.
ObjectManagerClient.setPersistenceFlag(oid,Boolean(True))
If the object persistence flag was previously false, the object will be marked dirty.
To disable the persistence flag on an existing object, call ObjectManagerClient.setPersistenceFlag(oid,false).
ObjectManagerClient.setPersistenceFlag(oid,Boolean(False))
If the object persistence flag was previously true, the object and all sub-objects will be deleted from the database. Note that the object will not be unloaded.
Local object persistence
The object manager enables you to save (persist) local objects to the database for later access. Persisting an object serializes it and sends it across the network to the object manager where it is saved in the database. You can persist any object that is an instance of Entity or a subclass thereof. Entity is a simple class that has an (optional) object ID and implements the Java serialization interface.
Saving an object
The multiverse/config/common/proxy.py script has an example of calling local object persistence in the testKeywordPersistence method of the TestCommand class.
def testKeywordPersistence(self, playerOid, key) :
testNamespace = Namespace.intern("testNamespace")
saveEntity = Entity("saveEntity")
saveEntity.setProperty("sample_property", "sample_value")
if (not ObjectManagerClient.saveObjectData(key,
saveEntity,
testNamespace)) :
Log.error("testKeywordPersistence failed on saving")
WorldManagerClient.sendObjChatMsg(playerOid, 0, "test failure")
return
The first part of the method shown above creates a new Entity object and sets a property name/value pair on the Entity with setProperty(). Then, the call to saveObjectData() serializes and persists the object to the database. You must provide three arguments:
- A unique string key by which the object can be retrieved later.
- The object to persist, which must be derived from
Entity. - The
Namespaceto use.
Retrieving an object
The following code illustrates how to load a persistent local object from the database.
loadEntity = ObjectManagerClient.loadObjectData(key)
if (loadEntity == None) :
Log.error("testKeywordPersistence failed on loading, got null")
WorldManagerClient.sendObjChatMsg(playerOid, 0, "test failure")
return
The call to loadObjectData() returns an Entity object. You must provide two arguments to this method:
- The string key with which the object was previously persisted.
- The
Namespacecontaining the object.
The remaining lines, below, retrieve the value of the property named "sample_property" originally stored in the Entity object and check that its value is the same as originally set.
val = loadEntity.getProperty("sample_property")
if (val != "sample_value") :
Log.error("testKeywordPersistence failed, value=" + val)
WorldManagerClient.sendObjChatMsg(playerOid, 0, "test failure")
return
Log.error("testKeywordPersistence: test passed")
WorldManagerClient.sendObjChatMsg(playerOid, 0, "test success")
Associating persistent data with players
Need engineering input here.
To associate persisted data with a player character, use the ObjectManagerClient methods:
-
saveObject()to persist data. -
loadObject()to load persisted data.
Unloading objects
To remove an object from memory, unload it. Doing so will despawn the object (making it no longer visible) and remove it from all server data structures. If the object is persistent, then you can subsequently unload it from the database. If the object is not persistent, unloading is the equivalent of deleting the object.
Unloading distributed objects
Use ObjectManagerClient.unloadObject(oid) to make the object manager unload a distributed object. Before unloading the master object, the object manager unloads each sub-object. The sub-objects can perform clean-up (for example, the world manager de-spawns the object) or save themselves to the database. Do not use this method to unload instances.
For example:
ObjectManagerClient.unloadObject(oid)
Unloading instances
Use InstanceClient.unloadInstance(instanceOid). to unload an instance. This method will also unload instance content.
For example:
InstanceClient.unloadInstance(instanceOid)
Unloading local objects
Local objects can be registered or unregistered. Register objects with Entity.registerEntityByNamespace(). Remove registered objects from the local process with
Entity.removeEntityByNamespace(). Remove an unregistered object by simply removing all references to it from process data structures.
Deleting objects
Use ObjectManagerClient.deleteObject()
to make the object manager delete a persistent object. Deleting an object removes it from memory and from the database.
The object manager deletes each sub-object before it deletes the master object. The sub-objects can perform clean-up (for example, de-spawn the object) or delete themselves from the database. The object manager automatically deletes all sub-objects from the database, so the sub-objects can skip that step for convenience. This is the recommended practice since the object manager can atomically delete the whole database object.
For example:
ObjectManagerClient.deleteObject(oid)
Do not use this method to delete instances.
Deleting instances
Delete instances using InstanceClient.deleteInstance(instanceOid). Deleting an instance also deletes the content of the instance (except players).
For example:
InstanceClient.deleteInstance(instanceOid)
