[ Team LiB ] Previous Section Next Section

21.7 The MudPlace Class

Example 21-6 is the MudPlace class that implements the RemoteMudPlace interface and acts as a server for a single place or room within the MUD. It is this class that holds the description of a place and maintains the lists of the people and items in a place and the exits from a place. This is a long class, but many of the remote methods it defines have simple or even trivial implementations. The go( ), createPlace( ), and linkTo( ) methods are among the more complex and interesting methods; they manage the network of connections between MudPlace objects.

Note that the MudPlace class is Serializable, so that a MudPlace (and all places it is connected to) can be serialized along with the MudServer that refers to them. However, the names and people fields are declared transient, so they are not serialized along with the place.

Example 21-6. MudPlace.java
package je3.rmi;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.io.*;
import java.util.*;
import je3.rmi.Mud.*;

/** 
 * This class implements the RemoteMudPlace interface and exports a
 * bunch of remote methods that are at the heart of the MUD.  The
 * MudClient interacts primarily with these methods.  See the comment
 * for RemoteMudPlace for an overview.
 * The MudPlace class is Serializable so that places can be saved to disk
 * along with the MudServer that contains them.  Note, however that the
 * names and people fields are marked transient, so they are not serialized
 * along with the place (because it wouldn't make sense to try to save
 * RemoteMudPerson objects, even if they could be serialized).
 **/
public class MudPlace extends UnicastRemoteObject 
    implements RemoteMudPlace, Serializable
{
    String placename, description;          // information about the place
    Vector exits = new Vector( );            // names of exits from this place
    Vector destinations = new Vector( );     // where the exits go to
    Vector things = new Vector( );           // names of things in this place
    Vector descriptions = new Vector( );     // descriptions of those things
    transient Vector names = new Vector( );  // names of people in this place
    transient Vector people = new Vector( ); // the RemoteMudPerson objects
    MudServer server;                       // the server for this place
    
    /** A no-arg constructor for de-serialization only.  Do not call it */
    public MudPlace( ) throws RemoteException { super( ); }
    
    /**
     * This constructor creates a place, and calls a server method
     * to register the object so that it will be accessible by name
     **/
    public MudPlace(MudServer server, String placename, String description) 
        throws RemoteException, PlaceAlreadyExists
    {
        this.server = server;
        this.placename = placename; 
        this.description = description;
        server.setPlaceName(this, placename);  // Register the place
    }
    
    /** This remote method returns the name of this place */
    public String getPlaceName( ) throws RemoteException { return placename; }
    
    /** This remote method returns the description of this place */
    public String getDescription( ) throws RemoteException {
        return description;
    }

    /** This remote method returns a Vector of names of people in this place */
    public Vector getNames( ) throws RemoteException { return names; }
    
    /** This remote method returns a Vector of names of things in this place */
    public Vector getThings( ) throws RemoteException { return things; }
    
    /** This remote method returns a Vector of names of exits from this place*/
    public Vector getExits( ) throws RemoteException { return exits; }

    /** 
     * This remote method returns a RemoteMudPerson object corresponding to
     * the specified name, or throws an exception if no such person is here 
     **/
    public RemoteMudPerson getPerson(String name) 
        throws RemoteException, NoSuchPerson
    {
        synchronized(names) {
            // What about when there are 2 of the same name?
            int i = names.indexOf(name);
            if (i == -1) throw new NoSuchPerson( );
            return (RemoteMudPerson) people.elementAt(i);
        }
    }
    
    /** 
     * This remote method returns a description of the named thing, or
     * throws an exception if no such thing is in this place.
     **/
    public String examineThing(String name) throws RemoteException, NoSuchThing
    {
        synchronized(things) {
            int i = things.indexOf(name);
            if (i == -1) throw new NoSuchThing( );
            return (String) descriptions.elementAt(i);
        }
    }
    
    /** 
     * This remote method moves the specified RemoteMudPerson from this place
     * in the named direction (i.e. through the named exit) to whatever place
     * is there.  It throws exceptions if the specified person isn't in this
     * place to begin with, or if they are already in the place through the 
     * exit or if the exit doesn't exist, or if the exit links to another MUD 
     * server and the server is not functioning.
     **/
    public RemoteMudPlace go(RemoteMudPerson who, String direction) 
        throws RemoteException, NotThere, AlreadyThere, NoSuchExit, LinkFailed
    {
        // Make sure the direction is valid, and get destination if it is
        Object destination;
        synchronized(exits) {
            int i = exits.indexOf(direction);
            if (i == -1) throw new NoSuchExit( );
            destination = destinations.elementAt(i);
        }
        
        // If destination is a string, it is a place on another server, so
        // connect to that server.  Otherwise, it is a place already on this
        // server.  Throw an exception if we can't connect to the server.
        RemoteMudPlace newplace;
        if (destination instanceof String) {
            try { 
                String t = (String) destination;
                int pos = t.indexOf('@');
                String url = t.substring(0, pos);
                String placename = t.substring(pos+1);
                RemoteMudServer s = (RemoteMudServer) Naming.lookup(url);
                newplace = s.getNamedPlace(placename);
            } 
            catch (Exception e) { throw new LinkFailed( ); } 
        }
        // If the destination is not a string, then it is a Place
        else newplace = (RemoteMudPlace) destination;
        
        // Make sure the person is here and get their name.  
        // Throw an exception if they are not here
        String name = verifyPresence(who);
        
        // Move the person out of here, and tell everyone who remains about it.
        this.exit(who, name + " has gone " + direction);
        
        // Put the person into the new place.  
        // Send a message to everyone already in that new place
        String fromwhere;
        if (newplace instanceof MudPlace) // going to a local place
            fromwhere = placename;
        else
            fromwhere = server.getMudName( ) + "." + placename;
        newplace.enter(who, name, name + " has arrived from: " + fromwhere);
        
        // Return the new RemoteMudPlace object to the client so they
        // know where they are now at.
        return newplace;
    }
    
    /** 
     * This remote method sends a message to everyone in the room.  Used to
     * say things to everyone.  Requires that the speaker be in this place.
     **/
    public void speak(RemoteMudPerson speaker, String msg) 
        throws RemoteException, NotThere
    {
        String name = verifyPresence(speaker);
        tellEveryone(name + ":" + msg);
    }
    
    /** 
     * This remote method sends a message to everyone in the room.  Used to
     * do things that people can see. Requires that the actor be in this place.
     **/
    public void act(RemoteMudPerson actor, String msg)
        throws RemoteException, NotThere
    {
        String name = verifyPresence(actor);
        tellEveryone(name + " " + msg);
    }

    /** 
     * This remote method creates a new thing in this room.
     * It requires that the creator be in this room.
     **/
    public void createThing(RemoteMudPerson creator,
                            String name, String description) 
        throws RemoteException, NotThere, AlreadyThere
    {
        // Make sure the creator is here
        String creatorname = verifyPresence(creator);
        synchronized(things) {
            // Make sure there isn't already something with this name.  
            if (things.indexOf(name) != -1) throw new AlreadyThere( );
            // Add the thing name and descriptions to the appropriate lists
            things.addElement(name);
            descriptions.addElement(description);
        }
        // Tell everyone about the new thing and its creator
        tellEveryone(creatorname + " has created a " + name);
    }
    
    /**
     * Remove a thing from this room.  Throws exceptions if the person
     * who removes it isn't themself in the room, or if there is no
     * such thing here.
     **/
    public void destroyThing(RemoteMudPerson destroyer, String thing) 
        throws RemoteException, NotThere, NoSuchThing
    {
        // Verify that the destroyer is here
        String name = verifyPresence(destroyer);
        synchronized(things) {
            // Verify that there is a thing by that name in this room
            int i = things.indexOf(thing);
            if (i == -1) throw new NoSuchThing( );
            // And remove its name and description from the lists
            things.removeElementAt(i);
            descriptions.removeElementAt(i);
        }
        // Let everyone know of the demise of this thing.
        tellEveryone(name + " had destroyed the " + thing);
    }

    /**
     * Create a new place in this MUD, with the specified name and description. 
     * The new place is accessible from this place through
     * the specified exit, and this place is accessible from the new place 
     * through the specified entrance.  The creator must be in this place
     * in order to create a exit from this place.
     **/
    public void createPlace(RemoteMudPerson creator,
                            String exit, String entrance, String name, 
                            String description) 
        throws RemoteException,NotThere,ExitAlreadyExists,PlaceAlreadyExists
    {
        // Verify that the creator is actually here in this place
        String creatorname = verifyPresence(creator);
        synchronized(exits) {  // Only one client may change exits at a time
            // Check that the exit doesn't already exist.
            if (exits.indexOf(exit) != -1) throw new ExitAlreadyExists( );
            // Create the new place, registering its name with the server
            MudPlace destination = new MudPlace(server, name, description);
            // Link from there back to here
            destination.exits.addElement(entrance);
            destination.destinations.addElement(this);
            // And link from here to there
            exits.addElement(exit);
            destinations.addElement(destination);
        }
        // Let everyone know about the new exit, and the new place beyond
        tellEveryone(creatorname + " has created a new place: " + exit);
    }
    
    /**
     * Create a new exit from this mud, linked to a named place in a named
     * MUD on a named host (this can also be used to link to a named place in 
     * the current MUD, of course).  Because of the possibilities of deadlock,
     * this method only links from here to there; it does not create a return
     * exit from there to here.  That must be done with a separate call.
     **/
    public void linkTo(RemoteMudPerson linker, String exit, 
                       String hostname, String mudname, String placename) 
        throws RemoteException, NotThere, ExitAlreadyExists, NoSuchPlace
    {
        // Verify that the linker is actually here 
        String name = verifyPresence(linker);
        
        // Check that the link target actually exists.  Throw NoSuchPlace if
        // not.  Note that NoSuchPlace may also mean "NoSuchMud" or
        // "MudNotResponding".
        String url = "rmi://" + hostname + '/' + Mud.mudPrefix + mudname;
        try {
            RemoteMudServer s = (RemoteMudServer) Naming.lookup(url);
            RemoteMudPlace destination = s.getNamedPlace(placename);
        }
        catch (Exception e) { throw new NoSuchPlace( ); }
        
        synchronized(exits) {
            // Check that the exit doesn't already exist.
            if (exits.indexOf(exit) != -1) throw new ExitAlreadyExists( );
            // Add the exit, to the list of exit names
            exits.addElement(exit);
            // And add the destination to the list of destinations.  Note that
            // the destination is stored as a string rather than as a
            // RemoteMudPlace.  This is because if the remote server goes down
            // then comes back up again, a RemoteMudPlace is not valid, but the
            // string still is.
            destinations.addElement(url + '@' + placename);
        }
        // Let everyone know about the new exit and where it leads
        tellEveryone(name + " has linked " + exit + " to " + 
                     "'" + placename + "' in MUD '" + mudname + 
                     "' on host " + hostname);
    }
    
    /**
     * Close an exit that leads out of this place.
     * It does not close the return exit from there back to here.
     * Note that this method does not destroy the place that the exit leads to.
     * In the current implementation, there is no way to destroy a place.
     **/
    public void close(RemoteMudPerson who, String exit) 
        throws RemoteException, NotThere, NoSuchExit
    {
        // check that the person closing the exit is actually here
        String name = verifyPresence(who);
        synchronized(exits) {
            // Check that the exit exists
            int i = exits.indexOf(exit);
            if (i == -1) throw new NoSuchExit( );
            // Remove it and its destination from the lists
            exits.removeElementAt(i);
            destinations.removeElementAt(i);
        }
        // Let everyone know that the exit doesn't exist anymore
        tellEveryone(name + " has closed exit " + exit);
    }
    
    /** 
     * Remove a person from this place.  If there is a message, send it to 
     * everyone who is left in this place.  If the specified person is not here,
     * this method does nothing and does not throw an exception.  This method
     * is called by go( ), and the client should call it when the user quits.
     * The client should not allow the user to invoke it directly, however.
     **/
    public void exit(RemoteMudPerson who, String message)
        throws RemoteException
    {
        String name;
        synchronized(names) {
            int i = people.indexOf(who);
            if (i == -1) return;
            names.removeElementAt(i);
            people.removeElementAt(i);
        }
        if (message != null) tellEveryone(message);
    }
    
    /** 
     * This method puts a person into this place, assigning them the
     * specified name, and displaying a message to anyone else who is in
     * that place.  This method is called by go( ), and the client should
     * call it to initially place a person into the MUD.  Once the person
     * is in the MUD, however, the client should restrict them to using go( )
     * and should not allow them to call this method directly.
     * If there have been networking problems, a client might call this method
     * to restore a person to this place, in case they've been bumped out.
     * (A person will be bumped out of a place if the server tries to send
     * a message to them and gets a RemoteException.)
     **/
    public void enter(RemoteMudPerson who, String name, String message) 
        throws RemoteException, AlreadyThere
    {
        // Send the message to everyone who is already here.
        if (message != null) tellEveryone(message);
        
        // Add the person to this place.
        synchronized (names) {
            if (people.indexOf(who) != -1) throw new AlreadyThere( );
            names.addElement(name);
            people.addElement(who);
        }
    }
    
    /**
     * This final remote method returns the server object for the MUD in which
     * this place exists.  The client should not allow the user to invoke this
     * method.
     **/
    public RemoteMudServer getServer( ) throws RemoteException {
        return server;
    }
    
    /** 
     * Create and start a thread that sends out a message to everyone in this
     * place.  If it gets a RemoteException talking to a person, it silently
     * removes that person from this place.  This is not a remote method, but
     * is used internally by a number of remote methods.
     **/
    protected void tellEveryone(final String message) {
        // If there is no one here, don't bother sending the message!
        if (people.size( ) == 0) return;
        // Make a copy of the people here now.  The message is sent
        // asynchronously and the list of people in the room may change before
        // the message is sent to everyone.
        final Vector recipients = (Vector) people.clone( );
        // Create and start a thread to send the message, using an anonymous
        // class.  We do this because sending the message to everyone in this
        // place might take some time, (particularly on a slow or flaky
        // network) and we don't want to wait.
        new Thread( ) {
                public void run( ) {
                    // Loop through the recipients
                    for(int i = 0; i < recipients.size( ); i++) {
                        RemoteMudPerson person =
                            (RemoteMudPerson)recipients.elementAt(i);
                        // Try to send the message to each one.
                        try { person.tell(message); } 
                        // If it fails, assume that that person's client or
                        // network has failed, and silently remove them from
                        // this place.
                        catch (RemoteException e) { 
                            try { MudPlace.this.exit(person, null); } 
                            catch (Exception ex) {  } 
                        }
                    }
                }
            }.start( );
    }
    
    /**
     * This convenience method checks whether the specified person is here.
     * If so, it returns their name.  If not it throws a NotThere exception
     **/
    protected String verifyPresence(RemoteMudPerson who) throws NotThere {
        int i = people.indexOf(who);
        if (i == -1) throw new NotThere( );
        else return (String) names.elementAt(i);
    }

    /**
     * This method is used for custom de-serialization.  Since the vectors of
     * people and of their names are transient, they are not serialized with
     * the rest of this place.  Therefore, when the place is de-serialized,
     * those vectors have to be recreated (empty).
     **/
    private void readObject(ObjectInputStream in) 
        throws IOException, ClassNotFoundException {
        in.defaultReadObject( );  // Read most of the object as normal
        names = new Vector( );    // Then recreate the names vector
        people = new Vector( );   // and recreate the people vector
    }                     
    
    /** This constant is a version number for serialization */
    static final long serialVersionUID = 5090967989223703026L;
}
    [ Team LiB ] Previous Section Next Section