FileDocCategorySizeDatePackage
MudPlace.javaAPI DocExample17604Mon Sep 22 13:30:32 BST 1997None

MudPlace.java

// This example is from _Java Examples in a Nutshell_. (http://www.oreilly.com)
// Copyright (c) 1997 by David Flanagan
// This example is provided WITHOUT ANY WARRANTY either expressed or implied.
// You may study, use, modify, and distribute it for non-commercial purposes.
// For any commercial use, see http://www.davidflanagan.com/javaexamples

import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.io.*;
import java.util.*;
import 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 itself
  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 themselves 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 an 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 allow one client to 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 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;
}