Continuum Logo SourceForge.net Logo


DiamondMUD: Architectural Notes

Object model

The programming paradigma within a MUD implementation using DiamondMUD is object-based, not class-based (like Java and most other modern programming languages). People unfamiliar with this difference should refer to Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems by Henry Lieberman or the Self Programming Language. Many other MUDs follow this approach, like LPMUD or MOO (correct me, if I'm wrong). Generally it can be said, that the object-based approach is superior in terms of modelling (it is for example simple to implement a class-based system in an object-based system, but not the other way around), but type-safety is very hard to get, which may be the reason why programming languages that are commercially used chose to be class-based.

Multiple inheritance

Each object has a list of prototypes. If a member is accessed on an object, which does not define this member, the object will try to delegate the query or call to one of its prototypes. The resolution is "depth-first, left-to-right", which basically means, the object queries the list of prototypes in left-to-right order; since the prototypes are themselves objects, they do recursively the same, which results in a depth-first resolution.

The interface of objects can be highly dynamic, as members and prototypes can be added and removed at runtime. This means, there is no such thing as type-safety (or compile- or link-time binding). The advantage is, that objects can be changed without the need to recompile or relink any other objects. The obvious disadvantage is, that removing important members of central prototypes can make the MUD quite dysfunctional. The system is more governed by social laws than by technical ones.

Members

An important concept to understand is what I call masking. Others would call it overriding, but I borrowed the term from LPMUD, partly because the term overriding is much more popular and in most peoples' minds strongly associated with methods. Masking universally applies to all members. When a member is changed on an object that is not the defining object of the member (meaning, the object owns the member by delegation), the member is not changed on the prototype, but cloned into the delegating object, and changed there. Summarized, copy-on-write.

There are two kinds of members: properties and methods. Properties define the state of the object, while methods define its functionality. Please note, that through the delegation mechanism, objects will only need memory for state and functionality that differs from their prototypical state and functionality. So, an elephant will only need memory space for its color, if it is not grey. All this is hidden from the user, he simply queries the elephant for its color, or sets its color, never bothered, whether the color is obtained from the elephant directly, or from the prototypical elephant.

Methods always execute on the logical self. That is, if the prototypical elephant defines a method blush, which temporarily adds 20% to its red color component, the color is the color of the self, i.e. the concrete elephant at hand. If the concrete elephant does not yet define its non-prototypical color, it will after this function is called (copy-on-write). It is clear, that if this were not so, all elephants would blush if we would call blush on a concrete elephant. Polymorphism in an object-based world means polymorphism for both methods and properties.

Commands

Objects have also an optional list of commands associated with them. Commands are basically syntactic structures mapped to methods. A command could be: throw [self] at [%1] -> throw(%1), defined on a rock, meaning: if a user types something like "throw rock at window" or "throw stone at door", and "rock" or "stone", respectively, can be mapped to the rock defining the command, the rock's throw method is called with everything following "at" as a (string) parameter. The method in this example would most likely try to resolve %1 to an object in the same environment as the rock, and then decide whether to smash the window or door.

Inventory and environment

All objects are in a tree with exactly one root. In some cases, this containment hierarchy has a world analogy: a bag contain items, a room contains furniture etc. In other circumstances the hierarchy is of organizational nature: a library contains prototypes of some sort, another object may contain objects with globally callable commands.

The parent of an object is called its environment, or short env, the children of an object are collectively called its inventory or inv. The terms "parent" and "children" is usually reserved for the prototype hierarchy: parents are prototypes of an object, and the object is child to its prototypes.

Objects can also contain references to objects that are neither their env nor in their inv. In this case, the references are stored as properties and are inherently weak, meaning they will not prevent an objects removal from the world.

Objects always have a name that does not nevessarily represent its name in the world. The name's purpose is, that coders can use path names to locate objects. Paths follow exactly UNIX conventions, for absolute and for relative paths.

Types

DiamondMUD, being class-less, supports a restricted number of types:

name code class category
int i java.lang.Integer value
long l java.lang.Long value
float f java.lang.Float value
double d java.lang.Double value
string s java.lang.String value
boolean b java.lang.Boolean value
object o mud.core.object (see below)
list a mud.core.list reference
map m mud.core.map reference

Value types are unproblematic: if a value property is read, the code works on a copy of the value. Assignment to this copy has no effect on the property. For object properties a special rule applies: while an object property is technically a reference type, it is handled like a value type. Readonly access to an object property does not protect the object, but only the reference. The object is protected by its own security specifiers.

Please note, that the code for list is a, not l - just think "array", or "array list".

The reference property dilemma

Let's talk about a concrete example: a mob prototype "Thug" has a map property "stats", containing entries such as "cha":2,"str":6 etc. A concrete mob "Bill" inherits Thug. Now look at the following pseudo code:

      map st = Bill->stats; // 1
      int cha = st["cha"];  // 2
      st["str"] = 7;        // 3
		

Up to line 2 there is no need to mask stats to Bill, because the code performs only read accesses. But in line 3 we need to mask stats, because otherwise we would change the prototype Thug. Unfortunately, the local reference st does not contain the needed information, that stats is defined on Thug and accessed through Bill.

To have the precompiler detect such things is unfeasible - in a more complicated example with conditionals and flow-control statements we would all but rewrite the Java compiler. My intention is to write a precompiler based merely on regular expressions.

A good solution seems, to return a more complicated structure on an access to a reference property, similar to:

      class prop_ref<T>
        prop<T> p;
        object defined_on;
        object accessed_through;
        void set(T value)
          p.set(value);
        T mask()
          if (defined_on != accessed_through)
            mask p to accessed_through, yielding p'
            p = p'
            defined_on = accessed_through
          return p.get();
        T get()
          return p.get();
		

Then the above code is translated to something like:

      prop_ref<map> st = Bill->stats;           // 1
      int cha = st.get().get("cha");            // 2
      st.mask().set("str", 7);                  // 3
		

On the unlikely event, that Bill is in fact also a prototype, and "Crazy Bill" an inheritor of Bill, the following code would still pose problems:

      map stCB = CrazyBill->stats;      // 1, stats obtained from Thug
      map stB = Bill->stats;            // 2, stats obtained from Thug
      stB["str"] = 5;                   // 3, Bill's str now 5, defined on Bill
      int str = stCB["str"]             // 4, (CrazyBill's?) str still 6... (stCB still bound to Thug)
      int str = CrazyBill->stats["str"] // 5, (CrazyBill's?) str now 5! (obtained from Bill)
		

This is not too bad yet, but it can get worse:

      stCB["cha"] = 1;                  // 6, CrazyBill now masks Thug's stats (stCB still bound to Thug!) ->
      int str = CrazyBill->stats["str"] // 7, CrazyBill's str now 6 again!
		

This is a bit freaky. So we might have to think about a better solution.

User Code

User code is all code written for a concrete MUD implementation. Here a short overview, how a MUD based on DiamondMUD comes to be:

  1. The creator runs DiamondMUD, going through the setup procedure, creating the database, the first user, and some bootstrap objects.
  2. The creator starts instantiating objects with the commands found in the builder command suite.
  3. He starts attaching properties to them, builds inheritance hierarchies, places them in the object tree etc.
  4. Now he wants to write some custom behavior for his objects: he starts writing user code in the editor.

The process of writing user code is as follows:

  1. The coder attaches a new method to the target object, with the createmethod command found in the builder command suite.
  2. He then edits the method, following the syntax outlined in the following paragraphs.
  3. After saving he tries to compile the method, fixes errors, tests its behavior etc.

Internals

Method objects are wrappers, like property objects. The actual "value" of a method object is its underlying codeObject. codeObject is an interface with one public method Object execute(Object self, Object... params). When a method is called on a MUD object, this execute method is called, with the object as self.

The precompiler wraps user code first into a execute method declaration, and then into a class declaration like public class codeObject# implements codeObject, where # is a serial number. The method object keeps a reference to the currently loaded codeObject and its serial number.

When a method is edited and recompiled, the precompiler changes the serial number of the class, and upon successful compilation redirects the method object to the new codeObject and updates its serial number.

Syntax

The syntax for writing user code is essentially Java 1.5 syntax. There are a couple of language additions however.

Member access

When referencing a member of an object, the following syntax is used:

      myObject->myProp;
      myObject->myMethod(myParam1, myParam2);
		

This syntax implies a delegating lookup for the property or method: if the member is not found on myObject, its parents are queried recursively.

The classic Java dot-syntax is still used for members of types (which are Jasva classes), e.g.:

      myObject.destroy();
      myObject.getProp("myProp").get();     // equivalent to myObject->myProp
      myObject.getMethod("myMethod).call(); // equivalent to myObject->myMethod()
		

Quick casts

Since properties are typed, casts are very common. A simplified cast syntax is therefore provided. Examples:

      int phoneNumber = bill->i:phoneNumber;          // 1
      list<string> aliases = knife->as:aliases;       // 2 
      object bag = self->o:findObject("bag");         // 3
		

The identifiers are the "code" values from section Types above. Note the syntax for generics: in the quick cast, there are no < >. Also note, that we write string. string is completely equivalent to String or java.lang.String, but is preferred to keep the casing of types consistent.

Indexers

For lists and maps, indexers are provided. Examples:

      int cha = (int)bill->m:stats["cha"];
      int cha = bill->mi:stats["cha"]; // legal, if the "stats" property was defined as map<int>
      map<int> combatSkills = bill->mmi:skills["combat"];
      int unarmedSkill = bill->mmi:skills["combat"]["unarmed"];
      string firstAlias = bill->as:aliases[0];
      object firstBag = bill->ao:findObjects("bag")[0];
		

Parameter Declarations

If a method takes parameters they may be declared in the first lines of code, like:

      @param string  req1
      @param list    req2
      @param boolean opt1 = false
      @param int     opt2 = 5
		

In this example, we have to required parameters, and two optional parameters. This tells the precompiler to add the following lines:

      // required parameters
      if (params.length < 2 || params.length $gt; 4)
        throw new InvalidNumberOfParametersException();
      String req1 = null; 
      if (!(params[0] instanceof String)
        throw new InvalidParameterTypeException();
      req1 = (String)params[0];
      list req2 = null; 
      if (!(params[1] instanceof list)
        throw new InvalidParameterTypeException();
      req2 = (list)params[1];
        
      // optional parameters
      boolean opt1 = false
      int opt2 = 5
      if (params.length > 2) {
        if (!(params[2] instanceof Boolean)
          throw new InvalidParameterTypeException();
        opt2 = (Boolean)params[2];
        if (params.length > 3) {
          if (!(params[3] instanceof Integer)
            throw new InvalidParameterTypeException();
          opt2 = (Integer)params[3];
        }
      }
		

Example

Here a short example for a method to drop one or all items in the inventory. It assumes that there is another mechanism that allows players to "hold" objects (to prevent them from being accidentally dropped).

      // list<string> drop(string name, boolean force=false)
      //   Drops the specified object, or all objects in the inventory to the 
      //   environment.
      //   @param name: if null, all objects are dropped.
      //   @param force: if true, even "held" objects are dropped.
      //   @return: display names of objects that could not be dropped,
      //     because they were held.
      
      @param string name
      @param boolean force = false
      
      list<string> held = new list<string>();
      if (name == null) {
        for (object o : self.getInv()) {
          if (force || !o->b:held)
            o.move(self.getEnv());
          else    
            held.add(o->s:displayName);
        }
      }
      else {
        object o = findObject(name);
        if (o != null && !o->b:held)
          o.move(self.getEnv());
        else
          held.add(o->s:displayName);
      }
      return held;
		

Security

There are two major topics that affect DiamondMUD security: a peculiar way of using the Java access modifiers, and a low-level access security mechanism.

Java access modifiers

As explained elsewhere, when users write code for any object, they implmenet in fact always the execute method of a code_object, in a package outside the core packages. This means among other things:

All sensitive portions of the code are put into one package, where all sensitive members are package private. When this is not feasible, a trick is used, where the sensitive members are made protected. In the package of the class that needs access to the defining class, a subclass is defined, which grants indirect package private access to the protected members in question. This might seem bad for performance, but in fact the trick is only used to get to static members, which are statically linked anyway.

Access security system

As far as objects and their members (properties, methods and commands) are concerned, DiamondMUD provides a low-level security system based on access levels. I decided against a group- or role-based security system, because I believe, that only a simple system will be used well: With a complicated role-based system with many roles and each user assigned to several of them, things get quickly difficult to administer, and people will grant too many rights to escape the tide of unwanted security exceptions.

If anybody wants security groups for certain things, like "news poster", "lib coder" or whatever, it is more efficient to equip the involved commands with the necessary extra checks. If we talk about clan memberships needed for access to certain rooms, we have definitely left the topic of access security and entered the realm of MUD building.

Access levels

In my opinion, four low-level access levels are enough. More fine-grained security should be implemented using different means, as argued above. Nevertheless the system allows for a maximum of 15 access levels, plus the reserved 0 for "impossible" or "not appliccable", whichever you prefer. 1 is the lowest access level, while 15 is the highest (which is at least granted to the administrator of the MUD).

My recommendation is, to live with only four access levels: player, builder, wizard, and admin. Player is clear. Builder can change create rooms, items, mobs, change their properties, program commands for them etc., but cannot affect most player properties - simply put, he cannot interfere with a player's game-playing, directly raise his experience points, change skills or hit points etc. Wizards can do the latter, and in fact almost everything, like simply commanding a player to fall unconscious, or strip him of everything and catapult him across the MUD. He can also code commands or objects that do the same. However, he cannot do some very subtle things like changing the permission level of a user, or deleting people from disk. This can only be done by administrators.

If you think, four access levels is little, think of a couple of properties and try to assign the required modification access level - the decision is already difficult enough with four access levels.

Effective access level

The access right of an executing thread is determined by its current effective access level (EAL). When a command is issued, the EAL is set to the user access level (UAL), f.i. 15 for an administrator. Methods have a "set access level" (SAL) specifier, which, if other than 0 changes the EAL for the duration of the execution of this method. it normally runs under the user's access level.

While I would have preferred to use UNIXesh terms like UID, EUID and SUID, the access levels are really not user ids, hence the above different but reminiscent naming. Sometimes I use the abbreviation RAL for "required access level".

Access specifiers

Access specifiers are different for objects, properties, methods, and commands.

objects

For objects there are four specifiers:

name description
extend RAL for adding new members to the object.
write RAL for full access: changing members, changing specifiers, destroying the object.
move RAL for moving the object in the containment hierarchy, i.e. write access to its env field.
proto RAL for using this object as a prototype. This is usually either 0 for non-library objects, or on a builder or wizard level for library objects (prototypes). This specifier makes a prototype a prototype. It is only tested, when this object is directly added as a prototype to an object by the add_proto method (also called by the public object contructor), and f.i. not when an existing child object is cloned, loaded from the database, or itself add_protoed. This means specifically, that when the proto specifier value is raised, this has no effect on already existing child objects. Setting the proto specifier from non-0 to 0 raises an exception, if the child_count of the object is greater than 0.
properties

For properties there are three specifiers:

name description
read RAL for reading the property. If the property contains an object reference, the access rights on the object are determined by the specifiers on the object. If the property contains a collection type, it applies the property's access specifiers to its elements.
mask RAL for masking this property. Masking is the process of copying a property, which was obtained by delegation, to the object on which the access was performed, and modifying the copy.
write RAL for full access: changing the property's value, deleting the property.
methods

For methods there are four specifiers:

name description
execute RAL for executing this method.
mask RAL for masking (overriding) this method.
write RAL for full access: changing the method's code, deleting the method.
sal The effective access level this method should run under. Normally this is 0, which signifies that the effective access level should not be changed when executing this method.
commands

For commands there are two specifiers:

name description
access RAL for accessing this command.
write RAL for full access: changing the command, deleting the command.


Valid XHTML 1.0!