/* ** j###t ########## #### #### ** j###t ########## #### #### ** j###T "###L J###" ** ######P' ########## ######### ** ######k, ########## T######T ** ####~###L #### ** #### q###L ########## .##### ** #### \###L ########## #####" */ package key; import key.primitive.*; import key.util.LinkedList; import key.util.FilteredEnumeration; import key.util.MultiEnumeration; import key.util.RecursiveEnumeration; import key.util.CircularBuffer; import key.collections.StringKeyCollection; import java.io.*; import java.util.Date; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.Hashtable; import java.util.Vector; import key.effect.*; import key.util.StringTokenizer; import key.commands.Go; /** * The player class holds all the player specific information, such as * eachs players title, password, and received email. */ public final class Player extends Container implements Targetable,CommandContainer,Interactive,Splashable { private static final long serialVersionUID = 8665452585164135888L; public static final int MAX_PLAN_LINES = 6; public static final int MAX_PLAN_BYTES = MAX_PLAN_LINES * 80; public static final int MAX_DESCRIPTION_LINES = 22; public static final int MAX_DESCRIPTION_BYTES = MAX_DESCRIPTION_LINES * 80; public static final int MAX_PROMPT_LENGTH = 20; public static final int MAX_TITLE_LENGTH = 60; public static final int MAX_PREFIX_LENGTH = 10; public static final int MAX_AKA_LENGTH = 20; public static final int MAX_IDLEMSG_LENGTH = 70; public static final int MAX_BLOCKMSG_LENGTH = 70; public static final int MAX_BLOCKINGMSG_LENGTH = 70; public static final int MAX_LAST_CONNECT_FROM_SITE_LENGTH = 70; public static final int MAX_LOGIN_SCRIPT_LENGTH = 70; public static final int MAX_CONNECT_MSG_LENGTH = 40; public static final int MAX_OLD_LOGIN_LENGTH = 50; public static final int MAX_MODE_LENGTH = 50; public static final int MAX_TERMINAL_NAME_LENGTH = 25; private static final AtomicElement[] ELEMENTS = { AtomicElement.construct( Player.class, String.class, "password", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the players password" ), AtomicElement.construct( Player.class, String.class, "prompt", "prompt", AtomicElement.PUBLIC_FIELD, "the players prompt", AtomicSpecial.StringLengthLimit( MAX_PROMPT_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "title", "title", AtomicElement.PUBLIC_FIELD, "a string that sometimes comes after the players name", AtomicSpecial.StringLengthLimit( MAX_TITLE_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "mode", "mode", AtomicElement.PUBLIC_FIELD, "a string that is prepended to every line typed", AtomicSpecial.StringLengthLimit( MAX_MODE_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "prefix", "prefix", AtomicElement.PUBLIC_ACCESSORS, "a string that sometimes comes before the players name", AtomicSpecial.StringLengthLimit( MAX_PREFIX_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "aka", "aka", AtomicElement.PUBLIC_FIELD, "'also known as', another name for the player", AtomicSpecial.StringLengthLimit( MAX_AKA_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "loginMsg", "loginMsg", AtomicElement.PUBLIC_FIELD, "Message appended to informs on login (prefix with ': ')", AtomicSpecial.StringLengthLimit( MAX_CONNECT_MSG_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "logoutMsg", "logoutMsg", AtomicElement.PUBLIC_FIELD, "Message appended to informs on logout (prefix with ': ')", AtomicSpecial.StringLengthLimit( MAX_CONNECT_MSG_LENGTH, false, true ) ), AtomicElement.construct( Player.class, TextParagraph.class, "description", "description", AtomicElement.PUBLIC_FIELD, "a description of the player", // a players description is limited to 22 lines and 80*22 bytes // also set in commands.Describe.java - redundant atm AtomicSpecial.TextParagraphLengthLimit( MAX_DESCRIPTION_BYTES, MAX_DESCRIPTION_LINES ) ), AtomicElement.construct( Player.class, TextParagraph.class, "plan", "plan", AtomicElement.PUBLIC_FIELD, "the players plan", AtomicSpecial.TextParagraphLengthLimit( MAX_PLAN_BYTES, MAX_PLAN_LINES ) ), AtomicElement.construct( Player.class, Gender.class, "gender", AtomicElement.PUBLIC_FIELD, "the players gender" ), AtomicElement.construct( Player.class, Boolean.TYPE, "quiet", AtomicElement.PUBLIC_FIELD, "if true, the player will never be 'beeped'" ), AtomicElement.construct( Player.class, Integer.TYPE, "age", AtomicElement.PUBLIC_FIELD, "the players age" ), AtomicElement.construct( Player.class, Integer.TYPE, "florins", //AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, AtomicElement.PUBLIC_FIELD, "the amount of money the player has" ), AtomicElement.construct( Player.class, Inventory.class, "inventory", AtomicElement.PUBLIC_FIELD, "the objects the player has" ), AtomicElement.construct( Player.class, NoKeyContainer.class, "objects", AtomicElement.PUBLIC_FIELD, "the individual objects the player has" ), AtomicElement.construct( Player.class, String.class, "forcedTerminal", "forcedTerminal", AtomicElement.PUBLIC_FIELD, "the terminal that the user has forced the use of", AtomicSpecial.StringLengthLimit( MAX_TERMINAL_NAME_LENGTH, false, false ) ), AtomicElement.construct( Player.class, String.class, "idleMsg", "idleMsg", AtomicElement.PUBLIC_FIELD, "the message output when the player is idle", AtomicSpecial.StringLengthLimit( MAX_IDLEMSG_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "blockMsg", "blockMsg", AtomicElement.PUBLIC_FIELD, "the message output when a message to this player is blocked", AtomicSpecial.StringLengthLimit( MAX_BLOCKMSG_LENGTH, false, true ) ), AtomicElement.construct( Player.class, String.class, "blockingMsg", "blockingMsg", AtomicElement.PUBLIC_FIELD, "the message output when this player is blocking all messages", AtomicSpecial.StringLengthLimit( MAX_BLOCKINGMSG_LENGTH, false, true ) ), AtomicElement.construct( Player.class, Duration.class, "timezone", AtomicElement.PUBLIC_FIELD, "the time-offset of this player compared to local time" ), AtomicElement.construct( Player.class, Boolean.TYPE, "isIdle", AtomicElement.PUBLIC_FIELD, "true if the player is currently idle" ), AtomicElement.construct( Player.class, Boolean.TYPE, "hidden", AtomicElement.PUBLIC_FIELD, "true if the player is currently hidden" ), AtomicElement.construct( Player.class, Boolean.TYPE, "liberated", AtomicElement.PUBLIC_FIELD, "true if the player can't be enrolled in a clan" ), AtomicElement.construct( Player.class, Boolean.TYPE, "brief", AtomicElement.PUBLIC_FIELD, "true if the player wants shorter output" ), AtomicElement.construct( Player.class, Boolean.TYPE, "privateEmail", AtomicElement.PUBLIC_FIELD, "true if the player wants an unlisted email address" ), AtomicElement.construct( Player.class, Boolean.TYPE, "citizen", AtomicElement.PUBLIC_FIELD, "true if the player is a citizen" ), AtomicElement.construct( Player.class, Boolean.TYPE, "expert", AtomicElement.PUBLIC_FIELD, "true if the player is in expert mode" ), AtomicElement.construct( Player.class, Boolean.TYPE, "hideTime", AtomicElement.PUBLIC_FIELD, "true if the player has hidden his logintime" ), AtomicElement.construct( Player.class, Boolean.TYPE, "newmail", AtomicElement.PUBLIC_FIELD, "true if the player might have unread mail (check on next login)" ), AtomicElement.construct( Player.class, Integer.TYPE, "titleTolerance", AtomicElement.PUBLIC_FIELD, "the largest number of people in the room with a verbose list" ), AtomicElement.construct( Player.class, DateTime.class, "banishedUntil", AtomicElement.PUBLIC_FIELD, "the date at which the player will be unbanished" ), AtomicElement.construct( Player.class, String.class, "banishType", "banishType", AtomicElement.PUBLIC_FIELD, "how the player is banished, 'S' for siteban too, 'B' otherwise", AtomicSpecial.StringLengthLimit( 1, false, false ) ), AtomicElement.construct( Player.class, String.class, "lastConnectFrom", "lastConnectFrom", AtomicElement.PUBLIC_FIELD, "the last site the player connected from", AtomicSpecial.StringLengthLimit( MAX_LAST_CONNECT_FROM_SITE_LENGTH, false, false ) ), AtomicElement.construct( Player.class, CommandList.class, "commands", AtomicElement.PUBLIC_FIELD | AtomicElement.ATOMIC, "the players personal command list" ), AtomicElement.construct( Player.class, CommandList.class, "specialCommands", AtomicElement.PUBLIC_FIELD, "the players custom command list" ), AtomicElement.construct( Player.class, Boolean.TYPE, "canSave", AtomicElement.PUBLIC_FIELD, "true iff the player has permission to write to disk" ), AtomicElement.construct( Player.class, Boolean.TYPE, "staff", AtomicElement.PUBLIC_FIELD, "true iff the player is a member of the talker staff" ), AtomicElement.construct( Player.class, MessageBox.class, "mailbox", AtomicElement.PUBLIC_FIELD | AtomicElement.ATOMIC, "the players local mailbox" ), AtomicElement.construct( Player.class, TimeStatistics.class, "loginStats", AtomicElement.PUBLIC_FIELD, "the players login statistics" ), AtomicElement.construct( Player.class, ConnectionStatistics.class, "connectionStats", AtomicElement.PUBLIC_FIELD, "the players connection statistics" ), AtomicElement.construct( Player.class, Friends.class, "friends", AtomicElement.PUBLIC_FIELD | AtomicElement.ATOMIC, "the players friend list" ), AtomicElement.construct( Player.class, Group.class, "prefer", AtomicElement.PUBLIC_FIELD | AtomicElement.ATOMIC, "the players common mis-tell preferences" ), AtomicElement.construct( Player.class, InformList.class, "inform", AtomicElement.PUBLIC_FIELD | AtomicElement.ATOMIC, "this player wants to know when this list of people are online" ), AtomicElement.construct( Player.class, Boolean.TYPE, "informEveryone", AtomicElement.PUBLIC_FIELD, "this player wants to know when everyone connects" ), AtomicElement.construct( Player.class, Clan.class, "clan", AtomicElement.PUBLIC_FIELD, "the clan that the player is in" ), AtomicElement.construct( Player.class, Room.class, "home", AtomicElement.PUBLIC_FIELD, "the players home room" ), AtomicElement.construct( Player.class, Room.class, "location", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the players current location" ), AtomicElement.construct( Player.class, Realm.class, "realm", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the players current realm" ), AtomicElement.construct( Player.class, Room.class, "lastPublicRoomLocation", AtomicElement.PUBLIC_FIELD, "the players last location while in a public room" ), AtomicElement.construct( Player.class, Room.class, "loginRoom", AtomicElement.PUBLIC_FIELD, "the players requested login room" ), AtomicElement.construct( Player.class, Boolean.TYPE, "saved", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "true if this player has been written to disk (at least once)" ), AtomicElement.construct( Player.class, Boolean.TYPE, "beyond", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "true if this player has beyond access" ), AtomicElement.construct( Player.class, Integer.TYPE, "scapeCount", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the number of scapes this player is in" ), AtomicElement.construct( Player.class, Integer.TYPE, "rankCount", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the number of ranks this player is in" ), AtomicElement.construct( Player.class, InteractiveConnection.class, "connection", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY, "the connection for this player (or null if not connected)" ), AtomicElement.construct( Player.class, String.class, "titledName", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY | AtomicElement.GENERATED, "the name and title of this player" ), AtomicElement.construct( Player.class, String.class, "possessiveName", AtomicElement.PUBLIC_ACCESSORS | AtomicElement.READ_ONLY | AtomicElement.GENERATED, "the name of this player with 's appended" ), AtomicElement.construct( Player.class, EmailAddress.class, "email", AtomicElement.PUBLIC_FIELD, "the email address of this player" ), AtomicElement.construct( Player.class, Webpage.class, "homepage", AtomicElement.PUBLIC_FIELD, "the homepage of this player" ), AtomicElement.construct( Player.class, Site.class, "bannedWithSite", AtomicElement.PUBLIC_FIELD, "the site that was banned along with this player" ), AtomicElement.construct( Player.class, Boolean.TYPE, "showFlags", AtomicElement.PUBLIC_FIELD, "if this player is seeing tell codes " ), AtomicElement.construct( Player.class, String.class, "oldLoginTime", "oldLoginTime", AtomicElement.PUBLIC_FIELD, "old EW forest login time", AtomicSpecial.StringLengthLimit( MAX_OLD_LOGIN_LENGTH, false, false ) ), AtomicElement.construct( Player.class, String.class, "loginScript", "loginScript", AtomicElement.PUBLIC_FIELD, "pipe seperated list of commands to be executed at login", AtomicSpecial.StringLengthLimit( MAX_LOGIN_SCRIPT_LENGTH, false, false ) ), }; // this class is immutable and final, therefore this is safe public static final AtomicStructure STRUCTURE = new AtomicStructure( Container.STRUCTURE, ELEMENTS ); public static final int DEFAULT_MAX_OBJECTS = 4; public static final int DEFAULT_MAX_MAIL = 20; public static final int DEFAULT_MAX_FRIENDS = 50; public static final int DEFAULT_MAX_PREFER = 10; public static final int DEFAULT_MAX_INFORM = 50; public static final int MAX_NAME = 16; public static final int MIN_NAME = 3; public static final int SHOWFLAG_DIRECTED = 0; public static final int SHOWFLAG_FRIENDS = 1; public static final int SHOWFLAG_ROOM = 2; public static final int SHOWFLAG_SHOUTS = 3; public static final int SHOWFLAG_ENTER = 4; public static final int SHOWFLAG_LEAVE = 5; public static final int SHOWFLAG_LOGIN = 6; public static final int SHOWFLAG_LOGOUT = 7; public static final int SHOWFLAG_BLOCKING = 8; // name blocks the clan chan public static final char[] SHOWCHARS = { '>', '*', '-', '!', ')', '(', '}', '{', '#' }; /** The players connection */ transient InteractiveConnection ic; /** The players location */ // some confusion about this vs the property - I think the // property might be the 'last public room' location. transient Room location; transient Reference realm; /** The current context for the player */ transient Atom context; /** a reverseReference style linked list of the scapes this player is in */ transient Vector reverseScapes; public static final int MAX_HISTORY_LINES = 10; /** a list of communication to/from this player */ private transient CircularBuffer history; /** * This is not the definitive reference as to the ranks a player is in, * although it is reasonable to expect that it is up to date. This * data structure gets updated by Rank.java as players are added and * removed to a rank. It is used to load Ranks into memory as a player * comes online, allowing them to detect that the Player has come online, * and link them into the rank if they wish. */ Vector reverseRanks = new Vector( 5, 5 ); /** a list of the people in the multi to reply to */ transient LWPlayerGroup replyList; /** a linkedList of qualifier ref's */ QualifierList qualifierList; /** Whether the player is 'beyond' (account access) or not */ private boolean beyond; private boolean canPage = true; boolean saved; public String getName() { //if( saved ) return( super.getName() ); //else //return( "VISITOR:" + super.getName() ); } boolean showFlags; boolean wordwrap = true; //--- system accessable properties ---// private Password password = new Password(); String prompt = "-> "; String title = " the newbie, so please treat me nicely"; String mode = null; String prefix = ""; String aka = ""; TextParagraph description = new TextParagraph(); TextParagraph plan = new TextParagraph(); Gender gender = Gender.NEUTER_GENDER; boolean quiet = false; int age = 0; int florins = 0; String forcedTerminal = null; String idleMsg = ""; String blockMsg = ""; String blockingMsg = ""; String loginMsg = ""; String logoutMsg = ""; Duration timezone = new Duration(); boolean isIdle = false; boolean hidden = false; boolean liberated = false; boolean brief = false; boolean privateEmail = true; boolean citizen = false; boolean expert = false; boolean hideTime = false; boolean newmail = false; boolean informEveryone = false; int titleTolerance = 10; DateTime banishedUntil = null; String banishType = ""; String lastConnectFrom = ""; Reference commands = Reference.EMPTY; Reference specialCommands = Reference.EMPTY; String oldLoginTime = ""; boolean canSave = true; boolean staff = false; // it isn't a security violation to have these fields public, // as they are final, and they look after their own immutability. public final TimeStatistics loginStats = (TimeStatistics) Factory.makeAtom( TimeStatistics.class, "loginStats" ); public final ConnectionStatistics connectionStats = (ConnectionStatistics) Factory.makeAtom( ConnectionStatistics.class, "connectionStats" ); public final Friends friends = (Friends) Factory.makeAtom( Friends.class, "friends" ); public final Group prefer = (Group) Factory.makeAtom( Group.class, "prefer" ); public final InformList inform = (InformList) Factory.makeAtom( InformList.class, "inform" ); public final Inventory inventory = (Inventory) Factory.makeAtom( Inventory.class, "inventory" ); public final NoKeyContainer objects = (NoKeyContainer) Factory.makeAtom( NoKeyContainer.class, "objects" ); public final MessageBox mailbox = (MessageBox) Factory.makeAtom( MessageBox.class, "mailbox" ); Reference clan = Reference.EMPTY; Reference room = Reference.EMPTY; Reference lastPublicRoomLocation = Reference.EMPTY; Reference loginRoom = Reference.EMPTY; Reference home = Reference.EMPTY; String loginScript = "orient|l"; public boolean getSaved() { return( saved ); } public boolean getBeyond() { return( beyond ); } public int getScapeCount() { return( reverseScapes.size() ); } public int getRankCount() { return( reverseRanks.size() ); } EmailAddress email = new EmailAddress(); Webpage homepage = new Webpage(); Reference bannedWithSite = Reference.EMPTY; //--- end accessable properties ---// static char originatorCode = 'o'; /** Codes used for % replacements when this player does a command */ transient String[] codes; /** Used to store the information which allows the 'repeat' command to work */ transient Repeated repeat; // for detecting the player idling out in the latencycache private transient LatentlyCached latentCacheHook; /** Constructs a player */ Player() { setLimit( DEFAULT_MAX_OBJECTS ); ic = null; location = null; realm = Key.instance().getDefaultRealm().getThis(); beyond = false; saved = false; // this now static //originatorCode = new Character( 'o' ); init(); qualifierList = new QualifierList(); showFlags = true; mailbox.setLimit( DEFAULT_MAX_MAIL ); friends.setLimit( DEFAULT_MAX_FRIENDS ); prefer.setLimit( DEFAULT_MAX_PREFER ); inform.setLimit( DEFAULT_MAX_INFORM ); // just a default so that a new player *has* some commands... commands = ((Atom) Key.instance().commandSets.getElement( "base" )).getThis(); // set up some default permissions // generally, its okay to talk to people & friend them permissionList.allow( tellAction ); permissionList.allow( friendAction ); // and its okay to send them email mailbox.getPermissionList().allow( Container.addToAction ); inventory.getPermissionList().allow( Container.addToAction ); } private void readObject( ObjectInputStream ois ) throws IOException { try { ois.defaultReadObject(); } catch( ClassNotFoundException e ) { throw new UnexpectedResult( e.toString() ); } init(); // Playerfile revision code (example) // We just added loginRoom, which is a Reference, // and we want it to default to Reference.EMPTY. if( loginRoom == null ) loginRoom = Reference.EMPTY; if( loginMsg == null ) loginMsg = ""; if( logoutMsg == null ) logoutMsg = ""; } private void init() { reverseScapes = new Vector( 5, 5 ); codes = new String[26]; idleTicks = MAX_IDLE_TICKS; context = this; idlePrompt = ""; repeat = new Repeated(); spamThrottle = new long[ NUMBER_OF_THROTTLES ]; spamViolations = new int[ NUMBER_OF_THROTTLES ]; if( history == null ) history = new CircularBuffer( MAX_HISTORY_LINES ); totalPlayers++; commandLoopCount = 0; } public Enumeration getTellHistory() { if( history != null ) { if( getCurrent() != this ) throw new AccessViolationException( this, "No-one may access another players tell history" ); return( history.elements() ); } return( key.util.EmptyEnumeration.EMPTY ); } public AtomicStructure getDeclaredStructure() { return( STRUCTURE ); } void stopBeingTemporary() { // do nothing, we upgrade from temporary to distinct later //index = Registry.instance.allocateTemporaryIndex( this ); } /** * Players always own themselves */ void assignInitialOwner() { owner = getThis(); } public final void putCode( char c, String value ) { try { codes[ c - 'a' ] = value; } catch( Exception e ) { e.printStackTrace(); Log.error( "During Player::putCode()", e ); } } public final String[] getCodes() { return( codes ); } public boolean brief() { return( brief ); } public void setBrief( boolean nb ) { permissionList.check( modifyAction ); brief = nb; } public boolean isBeyond() { return( beyond ); } public boolean isExpert() { return( expert ); } public void setWordwrap( boolean ww ) { wordwrap = ww; if( connected() ) { if( ic instanceof TelnetIC ) ((TelnetIC)ic).setWordwrap( ww ); else throw new WrongTerminalTypeException(); } } public boolean getWordwrap() { return( wordwrap ); } // required for the 'term' command. public synchronized boolean setTerminal( String name ) { permissionList.check( modifyAction ); if( connected() ) { if( ic instanceof TelnetIC ) { boolean r = ((TelnetIC)ic).forceTerminal( name ); if( r ) forcedTerminal = name; return( r ); } else throw new WrongTerminalTypeException(); } else throw new PlayerNotConnectedException(); } // required for the 'term' command. public synchronized void resetTerminal() { permissionList.check( modifyAction ); if( connected() ) { if( ic instanceof TelnetIC ) ((TelnetIC)ic).resetTerminal(); else throw new WrongTerminalTypeException(); } forcedTerminal = null; } public Terminal getTerminal() { if( connected() ) if( ic instanceof TelnetIC ) return( ((TelnetIC)ic).getTerminal() ); else throw new WrongTerminalTypeException(); else throw new PlayerNotConnectedException(); } // this would be an invasion of privacy to export public Repeated getRepeated() { // META: this modifyAction is temporary until we have some kind // of action to support this permissionList.check( modifyAction ); return( repeat ); } public void addMail( Letter l ) throws NonUniqueKeyException,BadKeyException { mailbox.add( l ); newmail = true; if( connected() ) { ic.sendSystem( "New mail from " + l.from + ", '" + l.description + "'. Use 'read mail 1' to read this message." ); ic.flush(); } } public MessageBox getMailbox() { return( mailbox ); } public QualifierList.Immutable getImmutableQualifierList() { // this is not a security risk because ImmutableQL isn't // changeable return( qualifierList.getImmutable() ); } public QualifierList getQualifierList() { permissionList.check( modifyAction ); return( qualifierList ); } public final Clan getClan() { try { return( (Clan) clan.get() ); } catch( OutOfDateReferenceException e ) { clan = Reference.EMPTY; return( null ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".clan wrong (reset)", e ); clan = Reference.EMPTY; return( null ); } } public final Realm getRealm() { try { return( (Realm) realm.get() ); } catch( OutOfDateReferenceException e ) { realm = Key.instance().getDefaultRealm().getThis(); return( (Realm) realm.get() ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".realm wrong (reset)", e ); realm = Reference.EMPTY; return( null ); } } public void setCanPage( boolean v ) { permissionList.check( modifyAction ); canPage = v; } public String getIdleMsg() { return( idleMsg ); } // it is the inventories problem to prevent itself from being // modified by unauthorised people public Inventory getInventory() { return( inventory ); } public EmailAddress getEmail() { // this could be made better (immutable email address?) permissionList.check( seePrivateInfoAction ); permissionList.check( modifyAction ); return( email ); } /** * META: this needs to be updated for the new permissionlist * that has a boolean check method (that doesn't throw exceptions) * for efficiency. */ public String getEmailAddress() { try { if( privateEmail ) permissionList.check( seePrivateInfoAction ); return( email.toString() ); } catch( AccessViolationException e ) { return( "<private>" ); } } public Webpage getWebpage() { return( homepage ); } public String getBlockMsg() { return( blockMsg ); } public String getLoginMsg() { return( loginMsg ); } public String getLogoutMsg() { return( logoutMsg ); } public String getAka() { return( aka ); } public int getAge() { return( age ); } // this should be automatically determined public boolean isEmailPrivate() { return( privateEmail ); } public String getPassword() { return( password.toString() ); } public Password getActualPassword() { permissionList.check( modifyAction ); return( password ); } public String getBlockingMsg() { return( blockingMsg ); } public Paragraph getPlan() { return( plan ); } public Paragraph getDescription() { return( description ); } public final boolean isCitizen() { return( citizen ); } public final boolean getExpert() { return( expert ); } public final Duration getTimezone() { return( timezone ); } public final void setTimezone( Duration nd ) { permissionList.check( modifyAction ); timezone = nd; } public boolean isHiding() { return( hidden ); } public String getLastConnectFrom() { return( lastConnectFrom ); } public void setIdle( boolean tf ) { permissionList.check( modifyAction ); isIdle = tf; recalculateIdlePrompt(); } public void setHiding( boolean tf ) { permissionList.check( modifyAction ); hidden = tf; } /** if the player can't be put into a clan */ public boolean isLiberated() { return( liberated ); } public void setLiberated( boolean nl ) { permissionList.check( modifyAction ); liberated = nl; } public Room getLastPublicRoom() { try { return( (Room) lastPublicRoomLocation.get() ); } catch( OutOfDateReferenceException e ) { lastPublicRoomLocation = Reference.EMPTY; return( null ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".lastpublicroom wrong (reset)", e ); lastPublicRoomLocation = Reference.EMPTY; return( null ); } } public Room getHome() { try { return( (Room) home.get() ); } catch( OutOfDateReferenceException e ) { home = Reference.EMPTY; return( null ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".home wrong (reset)", e ); home = Reference.EMPTY; return( null ); } } public void setHome( Room r ) { permissionList.check( modifyAction ); try { home = r.getThis(); } catch( TypeMismatchException e ) { throw new UnexpectedResult( "property 'home' is not a Room in Player: " + e.toString() ); } } public Friends getFriends() { return( friends ); } public Group getPrefer() { return( prefer ); } public String getPrefix() { return( prefix ); } public void setPrefix( String s ) { if( s == null || s.length() == 0 ) prefix = ""; else prefix = s + " "; } public String getTitledName() { return( getName() + title ); } public InformList getInform() { return( inform ); } // should modify this routine to do // authentication - don't allow anyone // to set the password? public void setPassword( String newValue ) { permissionList.check( modifyAction ); password.set( newValue ); } void linkToScape( Scape s ) { reverseScapes.addElement( s ); } void unlinkFromScape( Scape s ) { reverseScapes.removeElement( s ); } void ejected( Room from, Effect enter, Effect leave ) { Room wanted = Key.instance().getConnectRoom( this ); if( leave != null ) leave.cause(); moveTo( wanted ); enter.cause(); roomLook(); } void addReverseRank( Rank r ) { reverseRanks.addElement( r.getThis() ); System.out.println( "added revrank '" + r.getName() + "' to player " + getName() ); } void removeReverseRank( Rank r ) { reverseRanks.addElement( r.getThis() ); System.out.println( "removed revrank '" + r.getName() + "' to player " + getName() ); } public Enumeration scapes() { return( reverseScapes.elements() ); } public Enumeration ranks() { return( new FilteredEnumeration( reverseRanks.elements(), new ReferenceEnumeratorFilter( new ReferenceEnumeratorFilter.EnumeratedThing() { public void noSideEffectRemove( Reference r ) throws NonUniqueKeyException, java.util.NoSuchElementException,BadKeyException { Player.this.reverseRanks.removeElement( r ); } } , true ) ) ); } /** * You can target a player if you can target * any of the ranks that they're in. That means * that most staff members will _not_ be in the * 'resident' rank (so they can't hurt each other. */ public boolean isOutRankedBy( Rank rank ) { // cache this value for speed Targets targets = rank.getTargets(); if( targets != null ) { for( Enumeration e = targets.elements(); e.hasMoreElements(); ) { Object o = e.nextElement(); if( o instanceof Rank ) { Rank theRank = (Rank) o; if( theRank.containsAtAll( getThis() ) ) { return true; } } else if( o == this ) return true; } } return false; } /** eg: "subtle's" */ // META: better representation for name would be nice, these are called a lot public final String getPossessiveName() { return( getName() + "'s" ); } /** Gets the players location */ public final Room getLocation() { if( isHiding() ) permissionList.check( findAction ); return( location ); } //public static final int MAX_IDLE_TICKS = 1; //public static final int MAX_IDLE_TICKS = 5; public static final int MAX_IDLE_TICKS = 10; private transient int idleTicks; /** * Called by the interactive connection to reset the * players idletime to 0 */ final void unIdle() { // don't you dare add anything to this routine to stop // certain players idling out: it needs to be as small // and fast as possible. Put it in 'resetModify', // if you must. connectionStats.unIdle(); idleTicks = MAX_IDLE_TICKS; } public final Duration getIdle( DateTime now ) { return( connectionStats.getIdleTime( now ) ); } /** A players aspect is its description when looked at */ public Paragraph aspect() { return( description ); } /** Called when a password entry is required */ public boolean authenticate( InteractiveConnection ic ) throws IOException,PasswordEntryCancelled { return( password.check( getName(), ic ) ); } private transient int connecting = 0; void beginConnect() { connecting++; } void endConnect() { if( connecting > 0 ) connecting--; } public void setContext( Atom r ) { permissionList.check( modifyAction ); if( r != null ) context = r; else context = this; } /** Returns the current players context */ public Atom getContext() { return( context ); } protected transient int lagAmount = 0; protected transient boolean lagOnce = false; protected transient boolean discard = false; public void setLag( int amount, boolean continual ) { permissionList.check( modifyAction ); if( amount >= 0 ) { lagAmount = amount * 1000; lagOnce = continual; } } /** The main thread of a player - simply starts the parser */ public void run( InteractiveConnection ic ) { //setPriority( Thread.NORM_PRIORITY-1 ); if( ic != getConnection() ) { ic.send( "Poor connection" ); ic.close(); return; } ic.setKey( "IC: " + getKey().toString() ); try { synchronized( this ) { ic.blankLines( 3 ); // connect the player to a room, initially { Room startRoom = null; try { startRoom = (Room) loginRoom.get(); } catch( Exception e ) { loginRoom = Reference.EMPTY; ic.sendFailure( "^hYour login room is no longer valid.^-" ); } if( startRoom == null ) startRoom = Key.instance().getConnectRoom( this ); String message = startRoom.getLoginMessage(); putCode( originatorCode, getName() ); message = Grammar.substitute( message, getCodes() ); moveTo( startRoom, null, new key.effect.Enter( this, startRoom, message ) ); } if( location == null ) { ic.sendError( "Could not connect you to a room, disconnecting..." ); disconnect(); return; } // scan ranks int warnLostRank = 0; for( int i = 0; i < reverseRanks.size(); i++ ) { Reference o = (Reference) reverseRanks.elementAt( i ); try { Rank r = (Rank) o.get(); if( !r.contains( this ) ) { reverseRanks.removeElementAt( i-- ); warnLostRank++; } else r.establish( this ); } catch( OutOfDateReferenceException e ) { reverseRanks.removeElementAt( i-- ); warnLostRank++; } catch( ClassCastException e ) { reverseRanks.removeElementAt( i-- ); warnLostRank++; } } // might as well let them know... if( warnLostRank > 0 ) { if( warnLostRank == 1 ) ic.send( "^hWhile you were offline, a rank that you were in was erased. (Just letting you know) You will not be notified of this again, and I have no way of determining which rank it was.^-" ); else ic.send( "^hWhile you were offline, " + warnLostRank + " ranks that you were in were erased. (Just letting you know) You will not be notified of this again, and I have no way of determining which ranks they were.^-" ); } // this is the bit that links the player back // into any groups they should be in. :) putCode( originatorCode, getName() ); new key.effect.Login( this, "%o has connected^@" + loginMsg + "^$" ).cause(); // execute the players login script { StringTokenizer st = new StringTokenizer( loginScript, "|", false ); commandLoopCount = 0; while( st.hasMoreTokens() ) { command( st.nextToken(), ic, false ); Thread.yield(); // be nice and friendly } } } try { String entered; while( connected() ) { // just a quick check for the lag code // *grin* if( lagAmount >= 0 ) { try { Thread.sleep( lagAmount ); } catch( InterruptedException e ) { } if( lagOnce == true ) { lagAmount = 0; lagOnce = false; if( discard ) { try { ic.discard(); } catch( IOException e ) { } discard = false; } } } if( mode == null ) entered = ic.input( getContextualPrompt() ); else { entered = ic.input( getContextualPrompt() + mode ); if( entered.length() == 0 ) mode = null; else entered = mode + entered; } Thread.yield(); // be nice and friendly if( entered.length() == 0 ) { if( ic.isPaging() ) { ((TelnetIC)ic).drawNextPage(); } continue; } try { commandLoopCount = 0; command( entered, ic, false ); } catch( Exception e ) { if( e instanceof UserOutputException ) ((UserOutputException)e).send( ic ); else throw e; } } } catch( NetworkException e ) { System.err.println( e.toString() ); e.printStackTrace(); } } catch( ThreadDeath td ) { synchronized( this ) { if( ic == getConnection() ) { disconnect(); ic = null; } else if( ic != null ) { ic.close(); ic = null; } } throw td; } catch( Exception e ) { Log.error( "Player::run() [" + getName() + "]", e ); } synchronized( this ) { // necessary for reconnections - we don't // want to disconnect the newly connected // player, but neither do we want to exit // this thread with an unclosed connection. if( connected() ) { if( ic == getConnection() ) disconnect(); else if( ic != null ) ic.close(); } else if( ic != null ) ic.close(); } } private transient String idlePrompt; private final void recalculateIdlePrompt() { if( isIdle ) idlePrompt = "[IDLE] "; else idlePrompt = ""; } public final boolean isIdle() { return( isIdle ); } private transient boolean isafk = false; public final void afk( boolean v ) { permissionList.check( modifyAction ); isafk = v; } public final boolean isAfk() { return( isafk ); } /** * Returns an ordered list of the commands that this * Player has access to. * * I believe this isn't being used. Enumeration getCommands() { // technically, this Observer might only be used // by the Commands class - so we probably should just // have a method that returns the above delayed enumeration return( new RecursiveEnumeration( getCommandListEnumerations(), false, // don't recurse deeply null // no observer required to just enumerate ) ); } */ /** * Returns an enumeration of enumerations of the * commands this player has access to. * * I believe this isn't being used */ Enumeration getCommandListEnumerations() { return( new FilteredEnumeration( getCommandLists(), new FilteredEnumeration.Filter() { public boolean isValid( Object element, Enumeration enum ) { return( true ); } public Object replace( Object element, Enumeration enum ) { if( element instanceof CommandList ) return( ((CommandList)element).elements() ); else return( null ); } } ) ); } /** * Returns an enumeration of enumerations of the * commands this player has access to. */ public Enumeration getCommandLists() { final Enumeration clist = new Enumeration() { int state = 0; int substate = 0; int last_substate = 0; Object next = scanNextElement(); public boolean hasMoreElements() { return( next != null ); } public Object nextElement() { if( next == null ) throw new NoSuchElementException(); Object o = next; next = scanNextElement(); return( o ); } /** * This is done like this because, believe it or not, * it's cleaner and more efficient this way. Really. * The old way sucked. * <P> * Because of the way this is written, it can dynamically * scan command lists without having to pre-process them, * but also through the Enumeration interface. */ public Object scanNextElement() { // temporary variable for result CommandList cl; switch( state ) { case 0: state++; // state 1 is next if( context != null && context instanceof CommandContainer ) { cl = ((CommandContainer)context).getCommandList(); if( cl != null && cl.count() > 0 ) return( cl ); } // proceed to next state now case 1: // special commands for the player // (overriddable only by context) state++; // state 3 is next cl = Player.this.getSpecialCommandList(); if( cl != null && cl != context && cl.count() > 0 ) return( cl ); case 2: state++; // state 3 is next if( context != Player.this ) { cl = Player.this.getCommandList(); if( cl != null && cl.count() > 0 ) return( cl ); } // proceed to next state now case 3: state++; // state 4 is next last_substate = substate; // save the old substate substate = 0; // initialise // proceed to next state now case 4: // return groups for each player based on // the substate, which is the group we're // currently upto int rss = reverseScapes.size() - 1; if( (substate) > rss ) { // this is an error, we should // already be on the next state - // just skip through state++; } else { if( substate == rss ) { // this is the last time we can do this state++; // next time we're in the next state } CommandContainer cc = ((CommandContainer)(reverseScapes.elementAt( substate++ ))); if( cc != null && cc != context ) { cl = cc.getCommandList(); if( cl != null ) return( cl ); else return( scanNextElement() ); } else return( scanNextElement() ); } case 5: // this is not a valid state. default: return( null ); } } /** * Used to get some information about what we're * enumerating through for the command list. * <P> * Returns a string of the form: * <BR> * [context] name - x commands * <P> * This method is called to get information about * the result of the <I>last</I> nextElement() call. * * @return formatted details string */ String getLastDetails() { switch( state - 1 ) { case -1: throw new UnexpectedResult( "Player::getLastDetails() before nextElement()" ); case 0: // state 0 is the context scan return( "context " + context.getName() + " - " + "" ); //break; case 1: break; case 2: break; case 3: break; default: throw new UnexpectedResult( "Player::getLastDetails(): invalid state" ); } return null; } }; return( clist ); } private String getContextualPrompt() { StringBuffer gprompt = new StringBuffer(); gprompt.append( idlePrompt ); if( context != this ) { gprompt.append( '[' ); gprompt.append( context.getName() ); gprompt.append( "] " ); } if( ic.isPaging() ) gprompt.append( "[PAGER] " ); gprompt.append( prompt ); return( gprompt.toString() ); } /** * Returns an enumeration of enumerations of the * modal commands this player has access to that * matches a certain prefix string. The enumeration * contains Object[2] in each element. The first * is the matched commandcontainer, the second is * the top level parent of that command container, * usually indicating the providing rank or scape. */ public Enumeration getCategoryCommandsMatching( java.util.StringTokenizer args ) { //if( !args.hasMoreTokens() ) //throw new UnexpectedResult( "No arguments to getCategoryCommandsMatching specified" ); //String first = args.nextElement(); Vector path = null; CommandList cl = null; Vector result = new Vector( 16, 16 ); if( args != null && args.hasMoreTokens() ) { path = new Vector( 6, 6 ); do { path.addElement( args.nextToken() ); } while( args.hasMoreTokens() ); } //if( path.size() == 0 ) //throw new UnexpectedResult( "No arguments to getCategoryCommandsMatching specified" ); scan: for( Enumeration e = getCommandLists(); e.hasMoreElements(); ) { cl = (CommandList) e.nextElement(); if( cl == null ) continue; CommandList top = cl; Object match = null; if( path != null ) { for( Enumeration f = path.elements(); f.hasMoreElements(); ) { match = cl.getExactElement( (String) f.nextElement() ); //ic.send( "DEBUG TODO new2 match is " + match ); if( match instanceof CommandContainer ) { if( f.hasMoreElements() ) { // set up the loop to repeat cl = ((CommandContainer)match).getCommandList(); if( cl == null ) continue scan; // no more from this modal } else break; // just an optimisation, not reqd. } else continue scan; // not enough in this modal } } Object[] o = new Object[2]; o[0] = match; o[1] = top; // if we get here, we have a matching CommandContainer result.addElement( o ); } if( path != null ) path.setSize( 0 ); return( result.elements() ); } private transient int commandLoopCount = 0; /** * This routine kind of doubles up with CategoryCommand, * but searches many more places than CategoryCommand * does. Searches the current context + others */ public void command( String fullLine, InteractiveConnection ic, boolean query ) { if( commandLoopCount > 5 ) { ic.sendError( "Too many command loops." ); return; } commandLoopCount++; StringTokenizer args = new StringTokenizer( fullLine.trim() ); Commandable match = null; String command; CommandList cl = null; Commandable last = null; Commandable lastUnique = null; Command.Match lastcm = null; StringTokenizer argsFor = null; int highest_loop_count = -1; if( !args.hasMoreTokens() ) return; command = args.nextToken(); // the first word // attempt to find a matching command: scan: for( Enumeration e = getCommandLists(); e.hasMoreElements(); ) { try { last = match; match = (Commandable) ((CommandList) e.nextElement()).getExactElement( command ); if( last != match ) lastUnique = last; //ic.send( "set last to " + last + ", new match is " + match ); } catch( ClassCastException t ) { Log.error( "during command matching", t ); match = null; } if( match != null ) { StringTokenizer args_backup = (StringTokenizer) args.clone(); if( match.recloneArgs() ) { // take a copy of the arguments // now in case we need to repass // them to the command if it doesn't // match later. argsFor = (StringTokenizer) args.clone(); } else argsFor = null; int loop_count = 0; do { if( highest_loop_count < loop_count ) { // this is the furthest we've matched // this command - get an error string // in case we don't find a good match Command.Match cm = match.getFinalMatch( this, args ); lastcm = cm; if( cm.match != match ) lastUnique = match; last = match; match = cm.match; //ic.send( "set 2 last to " + lastUnique + ", new match is " + match ); highest_loop_count = loop_count; } else { last = match; match = match.getMatch( this, args ); if( last != match ) lastUnique = last; //ic.send( "set 3 last to " + last + ", new match is " + match ); } if( match == last ) { if( argsFor != null ) args = argsFor; break scan; } else if( match == null ) { args = args_backup; continue scan; } else { if( match.recloneArgs() ) { // take a copy of the arguments // now in case we need to repass // them to the command if it doesn't // match later. argsFor = (StringTokenizer) args.clone(); } else argsFor = null; } } while( loop_count++ < 5 ); // I don't do infinite loops on general // principle in code like this. This // will limit the damage of a circular // routine. throw new LimitExceededException( "too many modal commands" ); } } // check the exits in the players current room if( match == null && location != null && lastcm == null ) { Object o = location.getElement( command ); if( o instanceof Exit ) { Go.useExit( this, (Exit) o, ic, args, null ); return; } } if( match == null && lastcm != null && lastcm.lastchance != null ) { if( lastcm.lastchance.recloneArgs() ) { last = match; match = lastcm.lastchance; if( last != match ) lastUnique = last; //ic.send( "set 4 last to " + last + ", new match is " + match ); if( argsFor != null ) args = argsFor; } } if( match != null ) { CategoryCommand caller; try { caller = (CategoryCommand) lastUnique; } catch( ClassCastException t ) { caller = null; } if( match.isDisabled() ) { ic.sendFailure( "This command has been temporarily deactivated" ); return; } if( query ) { if( caller != null ) ic.sendFeedback( caller.getWhichId() + " -> " + match.getWhichId() ); else ic.sendFeedback( match.getWhichId() ); return; } try { if( caller != null ) caller.runCommand( match, this, args, fullLine, ic, null ); else match.run( this, args, fullLine, caller, ic, null ); } catch( NotEnoughParametersException t ) { // do nothing other than signal an error // state (ie, return code != 0, if we // were in unix. we're not, so nothing) } catch( Exception t ) { if( connected() ) { if( t instanceof UserOutputException ) ((UserOutputException)t).send( ic ); else { ic.sendError( "Error: " + t.toString() ); if( expert ) ic.printStackTrace( t ); else t.printStackTrace(); } } } catch( Error t ) { ic.sendError( t.toString() ); if( expert ) ic.printStackTrace( t ); Log.error( t ); throw t; } } else { if( lastcm != null ) ic.sendError( lastcm.getErrorString() ); else ic.sendError( "Cannot find command '" + command + "'" ); } } /** true if the player is connected */ public boolean connected() { return( ic != null ); } public InteractiveConnection getConnection() { if( connected() ) return( ic ); else throw new PlayerNotConnectedException(); } /** true if the player has a location */ public boolean located() { return( location != null ); } /** * Called to connect a player to an interactive connection. * * <ul> * <li>splashes everyone on the program, letting them know this person * has connected. * <li>tells the Key object to link this player into the online players * <li>locks this players soul into memory * <li>updates the players statistics (tells the loginStats to begin * <li>tells the interactive connection which player to unidle (this) when * a command is entered. * </ul> */ public synchronized void connectTo( InteractiveConnection connection ) throws NonUniqueKeyException,BadKeyException { // safety. should almost certainly have the authenticate here permissionList.check( modifyAction ); if( connected() ) { throw new UnexpectedResult( "already connected" ); } Key.instance().linkPlayer( this ); // do this first - this is the one // that will throw the exceptions ic = connection; // this is the bit that restored their // terminal parameters to the overridden ones if( ic instanceof TelnetIC ) { TelnetIC tic = (TelnetIC) ic; if( forcedTerminal != null ) tic.forceTerminal( forcedTerminal ); tic.setCanPage( canPage ); tic.setWordwrap( wordwrap ); } { // only if they've connected before if( lastConnectFrom.length() > 0 && loginStats.lastConnection != null ) { ic.send( "\n\nYou last connected at " + loginStats.lastConnection.toString( this ) + ",\nfrom: " + lastConnectFrom ); } } // update the onSince time to now lastConnectFrom = ic.getFullSiteName(); loginStats.startConnection(); // begin un-idling the player ic.startUnIdling( this ); if( latentCacheHook == null ) { latentCacheHook = new LatentlyCached() { // don't you dare add anything to this routine to stop // certain players idling out: it needs to be as small // and fast as possible. Put it in 'resetModify' // if you must. public final boolean modified() { return( idleTicks > 0 ); } public void resetModify() { idleTicks--; // we used to have an idle warning here, but that // isn't a good idea (people use trigger macros on // it. Increased possible 'idle' time to one hour // to compensate for the lack. //if( idleTicks == 1 && connected() ) // ic.sendError( "\n\n" ); // //if( I don't want to idle out as often ) // make idleTicks bigger, and remember not to do it // every single time (idle everyone out, eventually) } public synchronized void deallocate() { if( connected() ) { ic.sendFeedback( "\n\n\nYou have been idle for too long, disconnecting." ); disconnect(); } } }; } Key.getLatencyCache().addToCache( latentCacheHook ); Thread.yield(); // exile: when they reconnect they are no longer idle isIdle = false; } public final void setAge( int na ) { permissionList.check( modifyAction ); age = na; } public final void setAka( String na ) { permissionList.check( modifyAction ); aka = na; } public final void setBlockMsg( String na ) { permissionList.check( modifyAction ); blockMsg = na; } public final void setCitizen( boolean ba ) { permissionList.check( modifyAction ); citizen = ba; } public final void setBlockingMsg( String na ) { permissionList.check( modifyAction ); blockingMsg = na; } private transient boolean disconnecting = false; /** * Called to terminate a players connection. * * <ul> * <li>stops time recording * <li>takes the player out of the room that they're in * <li>removes the player from the online list of players in Key * <li>splashes everyone that the player is disconnecting * <li>closese the interactive connection * <li>stops the thread * </ul> */ public synchronized void disconnect() { permissionList.check( modifyAction ); if( disconnecting ) return; if( connected() ) { try { ic.flush(); } catch( Exception e ) { } try { disconnecting = true; InteractiveConnection dcd = ic; Key.getLatencyCache().removeFromCache( latentCacheHook ); florins += (int) loginStats.getTimeSinceConnection().getHours(); loginStats.endConnection(); if( Key.isRunning() ) { { String message = location.getLogoutMessage(); putCode( originatorCode, getName() ); message = Grammar.substitute( message, getCodes() ); (new key.effect.Leave( this, location, message )).cause(); } (new key.effect.Logout( this, getName() + " has disconnected^@" + logoutMsg + "^$" )).cause(); } int rss = reverseScapes.size(); while( rss > 0 ) { Scape s = (Scape) reverseScapes.elementAt( 0 ); try { s.unlinkPlayer( this ); } catch( NonUniqueKeyException t ) { Log.error( "while removing ourselves from scapes", t ); reverseScapes.removeElement( s ); } catch( BadKeyException t ) { Log.error( "while removing ourselves from scapes", t ); reverseScapes.removeElement( s ); } // over engineering to prevent an infinite loop here int nrs = reverseScapes.size(); if( nrs == rss ) { Log.error( "did not seem to remove scape as intended: please analyse algorithm" ); reverseScapes.removeElement( s ); nrs = reverseScapes.size(); if( nrs == rss ) { Log.error( "as above, but even more curious" ); break; } } rss = nrs; } try { // sync it only if we're a temporary atom atm. if( Registry.instance.getStorageTypeIndex( index, timestamp ) == Registry.STORAGE_TEMPORARY ) sync(); } catch( IOException e ) { dcd.send( e.toString() + " occured while trying to save your character" ); Log.error( "while saving player " + getName(), e ); } finally { dcd.close(); } } catch( Exception t ) { Log.error( "during Player::disconnect", t ); } finally { ic.stopUnIdling(); ic.close(); ic = null; location = null; idleTicks = 0; if( !willSync() ) dispose(); disconnecting = false; } } else throw new UnexpectedResult( "disconnecting a non-connected player" ); } void setSaved() { saved = true; } public boolean willSync() { return( hasPassword() && canSave ); } public boolean hasPassword() { return( password.isSet() ); } public boolean getCanSave() { return( canSave ); } public boolean isStaff() { return( staff ); } public String getRank() { if( willSync() ) { //if( beyond ) //return( "director" ); //else if( isStaff() ) //return( "staff" ); //else if( isCitizen() ) return( "citizen" ); else return( "resident" ); } else return( "visitor" ); } void clearTransient() { super.clearTransient(); if( connected() ) disconnect(); } public void sync() throws IOException { if( canSave ) { if( hasPassword() ) { setSaved(); super.sync(); if( connected() ) ic.send( "Character saved." ); } else { if( connected() ) ic.send( "You have no password, so your character has not been saved." ); } } else { if( connected() ) ic.send( "You have not been given permission to save" ); } } public void moveTo( Room newLocation, Effect leave, Effect enter ) { if( !connected() ) throw new PlayerNotConnectedException(); permissionList.check( summonAction ); if( leave != null ) leave.cause(); moveTo( newLocation ); enter.cause(); } public void roomLook() { if( ic == null ) throw new PlayerNotConnectedException(); //permissionList.check( modifyAction ); Room l = getLocation(); if( brief() ) { ic.send( new TextParagraph( (String) getLocation().called ), false ); ic.send( l.who( this ), false ); } else ic.send( l.aspect( this ), false ); ic.blankLine(); ic.flush(); } void moveTo( Room newLocation ) { try { if( location != null ) location.unlinkPlayer( this ); location = newLocation; realm = newLocation.getRealm().getThis(); if( location != null ) { location.linkPlayer( this ); // if they're moving into a public // room, store that they're there, // so we can return them to it when // they type 'leave', or when they // reconnect. if( !(location.getParent() instanceof Player) ) lastPublicRoomLocation = location.getThis(); } } catch( BadKeyException e ) { Log.error( e ); } catch( NonUniqueKeyException e ) { Log.error( e ); } } public boolean getShowFlags() { return( showFlags ); } public final String himHer() { return( gender.himHer() ); } public final String HisHer() { return( gender.HisHer() ); } public final String hisHer() { return( gender.hisHer() ); } public final String HeShe() { return( gender.HeShe() ); } public final String heShe() { return( gender.heShe() ); } public final String maleFemale() { return( gender.maleFemale() ); } public final void setGender( Gender ng ) { gender = ng; } public final Gender getGender() { return( gender ); } public synchronized void enrolIntoClan( Clan newClan ) { Clan old = getClan(); // this will be a problem if we ever do // untrusted coding - they could simply // enrolIntoClan( null ) & enrolIntoClan( newC ) // to get around the whole thing. if( old != null && newClan != null ) throw new AccessViolationException( this, getName() + " is already in clan " + old.getName() + ", and cannot be enrolled into a new clan until they leave." ); else { if( newClan == null ) { clan = Reference.EMPTY; // check to ensure the old clan isn't still // on the inform list try { inform.removeClanInform( old ); } catch( BadKeyException e ) { } catch( NonUniqueKeyException e ) { } } else clan = newClan.getThis(); } } /** * This is a special accessor that allows limited access * to the supplied players florin count: only to increase * it. * * @to the player to give the florins to * @amount the amount of florins to transfer */ public void transferFlorins( Player to, int amount ) { if( amount > florins ) throw new InvalidArgumentException( getName() + " doesn't have that many florins" ); if( amount < 1 ) throw new InvalidArgumentException( "Unable to transfer negative amounts of florins" ); permissionList.check( modifyAction ); to.florins += amount; florins -= amount; // if they are connected tell them about the good deed. if( to.connected() ) to.send( getName() + " gives you " + amount + " silver florins." ); } public int getFlorins() { return( florins ); } public boolean canMakeFriend( Player p ) { // this one just does a straight check (doesn't check groups, ranks, // or whatever). //return( permissionList.check( p.getThis(), friendAction ) ); return( permissionList.permissionCheck( friendAction, false, false ) ); } public String getFullName() { return( prefix + getName() ); } /** * I hereby declare each player to be * a soverign state, owned by no * other. ;p~ * * taken out only for the reason that it isn't * strictly necessary and I wanted to make isOwner final * subtle, 05Nov98 * public boolean isOwner( Atom potential ) { if( potential == this ) return( true ); else return( false ); } */ /* public boolean isTellBlockingPlayer( Reference p ) { return( !permissionList.check( originator.getThis(), tellAction ) ); } */ public void splash( Effect e, SuppressionList s ) { super.splash( e, s ); QualifierList.Entry q = null; boolean bounce = false; char preChar = ' '; String message = e.getMessage( this ); Player originator = e.getOriginator(); if( e instanceof Communication ) { if( originator != this || e instanceof Broadcast ) { // if not allowed, splash a message back and finish if( !permissionList.check( originator.getThis(), tellAction ) ) { if( isBeyond() ) { if( s != null ) s.add( this, SuppressionList.SPECIFIC, "(beyond used to send anyway)" ); } else { if( s != null ) s.add( this, SuppressionList.SPECIFIC, blockMsg ); return; } } q = qualifierList.getEntryFor( Type.typeOf( e.getSplasher() ) ); if( q == null ) q = qualifierList.getEntryFor( Type.typeOf( e ) ); if( showFlags && originator != this ) { if( e instanceof Directed ) preChar = SHOWCHARS[ SHOWFLAG_DIRECTED ]; else if( e instanceof Broadcast ) { Atom rs = e.getSplasher(); if( rs instanceof Friends ) preChar = SHOWCHARS[ SHOWFLAG_FRIENDS ]; else if( rs instanceof Room ) preChar = SHOWCHARS[ SHOWFLAG_ROOM ]; else if( e instanceof Shout ) preChar = SHOWCHARS[ SHOWFLAG_SHOUTS ]; } } bounce = true; // tell them if a communication message fails } } else if( e instanceof Movement ) { if( showFlags && originator != this ) { if( e instanceof Enter ) preChar = SHOWCHARS[ SHOWFLAG_ENTER ]; else if( e instanceof Leave ) preChar = SHOWCHARS[ SHOWFLAG_LEAVE ]; } //q = qualifierList.getEntryFor( Type.typeOf( e ) ); q = qualifierList.getEntryFor( Type.MOVEMENT ); } else if( e instanceof key.effect.Connection ) { StringBuffer foundBy = new StringBuffer( message ); Reference pRef = originator.getThis(); boolean triggered = false; for( Enumeration en = inform.referenceElements(); en.hasMoreElements(); ) { Reference a = (Reference) en.nextElement(); // this is an optimisation - (this code gets called a lot) // if the Reference isn't loaded, there // isn't much chance that this list entry // counts. if you take this out, be sure // to put it back in on the final 'else' // clause, since it is required there. if( a.isLoaded() ) { if( pRef.equals( a ) ) { foundBy.append( " [inform]" ); triggered = true; } else if( clan.equals( a ) ) { foundBy.append( " [clan]" ); triggered = true; } else // if( a.isLoaded() ) <taken out, see above> { Atom j = a.get(); //if( j instanceof Scape && ((Scape)j).containsPlayer( originator ) ) if( j instanceof Scape ) { if( ((Scape)j).containsPlayer( originator ) ) { foundBy.append( " [" ); foundBy.append( j.getName() ); foundBy.append( "]" ); triggered = true; } } } } } if( !triggered ) return; message = foundBy.toString(); if( showFlags && originator != this ) { if( e instanceof Login ) preChar = SHOWCHARS[ SHOWFLAG_LOGIN ]; else if( e instanceof Logout ) preChar = SHOWCHARS[ SHOWFLAG_LOGOUT ]; } q = qualifierList.getEntryFor( Type.CONNECTION ); } else if( e instanceof Blocking ) { q = qualifierList.getEntryFor( Type.BLOCKING ); if( showFlags && originator != this ) preChar = SHOWCHARS[ SHOWFLAG_BLOCKING ]; if( q == null && e instanceof Block ) { Scape sc = ((Block)e).blockOf(); q = qualifierList.getEntryFor( Type.typeOf( sc ) ); } } else if( e instanceof Global ) { ic.send( "\n\n" ); ic.send( new HeadingParagraph( message ) ); ic.send( "\n\n" ); ic.flush(); return; } else ic.send( " --[ Unknown splash type recvd ]--" ); if( q != null ) { char c = q.get(); if( c == Qualifiers.UNKNOWN_CODE ) sendSplash( preChar, message, s, e ); else if( c == Qualifiers.SUPPRESSION_CODE ) { //Log.debug( this, "suppressing message, bounce is " + bounce + ", sl is " + s.toString() ); // beyond goes through for directed events if( !(e.getOriginator().isBeyond() && e instanceof Directed ) ) { // this has been suppressed - splash a message back if( bounce && s != null ) s.add( this, SuppressionList.GENERAL, "(beyond used to deliver message anyway)" ); return; } else { // this has been suppressed - splash a message back if( bounce && s != null ) s.add( this, SuppressionList.GENERAL, getBlockingMsg() ); c = q.getMark(); if( c != Qualifiers.UNKNOWN_CODE ) sendSplash( preChar, "^" + q.getMark() + message, s, e ); else sendSplash( preChar, message, s, e ); } } else sendSplash( preChar, "^" + c + message, s, e ); } else sendSplash( preChar, message, s, e ); ic.flushIfWaiting(); } public void addHistoryLine( String s ) { history.addElement( s ); } public void beep() { if( connected() && !quiet ) ic.beep(); } private final void sendSplash( char preChar, String message, SuppressionList sl, Effect e ) { e.sending( message, this ); if( preChar == ' ' ) ic.send( message ); else ic.send( preChar, message ); if( sl != null ) { if( isIdle() || isAfk() ) sl.add( this, SuppressionList.IDLING, idleMsg ); } } public void sendSystem( String message ) { ic.send( message ); ic.flush(); } /** * This function is used to output direct, * *successful* feedback of a players command. * It is important that this feedback is normal * and expected by the player, since it generally * won't be hilited in any way. Please try * to restrict this output to a single line. * <p> * this command does start a new line * <p> * Examples: * <p> * You tell name 'hi' <br> * * @param message the text to be displayed */ public void sendFeedback( String message ) { ic.send( message ); ic.flush(); } /** * This function is used when the user makes some * sort of error in typing - *not* if a command * can't be executed because of a failure. So, * Command not found would go here, but 'Not enough * privs to nuke snapper' wouldn't. * <p> * this command does start a new line */ public void sendError( String message ) { ic.send( message ); ic.flush(); } public void sendFailure( String message ) { ic.sendFailure( message ); ic.flush(); } /** * Meant to be used to send part of what would * normally be a paragraph. */ public void send( String message ) { ic.send( message ); ic.flush(); } final void maybeClearBeyond() { if( !Key.instance().isRunning() ) beyond = false; else throw new UnexpectedResult( "trying to clear beyond while running" ); } final void maybeSetBeyond() { if( !Key.instance().isRunning() ) { beyond = true; expert = true; } else throw new UnexpectedResult( "trying to set beyond while running" ); } /** * Returns the number of rooms the player has */ public final int countRooms() { int i = 0; for( Enumeration e = elements(); e.hasMoreElements(); ) { if( e.nextElement() instanceof Room ) i++; } return( i ); } //--- commands ---// /** * A shortcut, since matching it from the * string could be prohibitively slow */ public final CommandList getCommandList() { try { return( (CommandList) commands.get() ); } catch( OutOfDateReferenceException e ) { commands = Reference.EMPTY; return( null ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".commands wrong (reset)", e ); commands = Reference.EMPTY; return( null ); } } public final CommandList getSpecialCommandList() { try { return( (CommandList) specialCommands.get() ); } catch( OutOfDateReferenceException e ) { specialCommands = Reference.EMPTY; return( null ); } catch( NullPointerException e ) { Log.error( "during getSpecialCommandList: this is once-only code for converting pfiles", e ); specialCommands = Reference.EMPTY; return( null ); } catch( ClassCastException e ) { Log.error( "somebody set " + getId() + ".specialcommands wrong (reset)", e ); commands = Reference.EMPTY; return( null ); } } //--- actions ---// protected static StringKeyCollection staticActions; public static Action tellAction; public static Action findAction; public static Action friendAction; public static Action summonAction; public static Action seePrivateInfoAction; static { staticActions = new StringKeyCollection(); findAction = newAction( Player.class, staticActions, "find", false, false ); friendAction = newAction( Player.class, staticActions, "friend", false, false ); seePrivateInfoAction = newAction( Player.class, staticActions, "seePrivateInfo", false, false ); summonAction = newAction( Player.class, staticActions, "summon", false, false ); tellAction = newAction( Player.class, staticActions, "tell", false, false ); } public Enumeration getActions() { return( new MultiEnumeration( staticActions.elements(), super.getActions() ) ); } public boolean containsAction( Action a ) { return( staticActions.contains( a ) || super.containsAction( a ) ); } /** * Returns the action corresponding to the * supplied name. This routine will need to * be overriden by sub-classes using actions. */ public Action getAction( String name ) { Action a = (Action) staticActions.get( name ); if( a == null ) return( super.getAction( name ) ); else return( a ); } private static int totalPlayers; public static int getTotalPlayers() { return( totalPlayers ); } protected void finalize() throws Throwable { super.finalize(); totalPlayers--; } boolean canSwap() { return( !(connected() || (connecting > 0)) ); } // property routines public boolean isBanished() { DateTime now = new DateTime(); if( banishedUntil == null ) return( false ); if( now.getTime() > banishedUntil.getTime() ) { banishedUntil = null; banishType = ""; return( false ); } return( true ); } public boolean checkNewMail() { return( newmail ); } public void clearNewMail() { newmail = false; } public void sendExamineScreen( InteractiveConnection ic ) { boolean seeThrough = permissionList.permissionCheck( seePrivateInfoAction, false, true ); Player p = getCurrent(); ic.sendLine(); ic.send( getTitledName() ); // show the description { if( description != null ) { ic.sendLine(); ic.send( description ); } } ic.sendLine(); if( connected() ) { ic.send( getFullName() + " has been logged in for " + loginStats.getTimeSinceConnection() ); InteractiveConnection pic = getConnection(); if( pic instanceof SocketIC ) ic.send( getName() + " is connected from: " + ((SocketIC)pic).getFullSiteName() ); } else { DateTime dateTime = (DateTime) loginStats.getLastConnection(); if( dateTime == null ) ic.send( getFullName() + " has never logged in." ); else ic.send( getFullName() + " was last seen at " + dateTime.toString( p ) ); } ic.sendLine(); // output ranks. ic.send( Grammar.enumerate( new FilteredEnumeration( ranks(), new key.util.FilteredEnumeration.Filter() { public boolean isValid( Object element, Enumeration enum ) { return( element instanceof Rank); } public Object replace( Object element, Enumeration enum ) { return( ((Rank)element).getContainedId() ); } }) ) ); /* for( Enumeration e = ranks(); e.hasMoreElements(); ) { Rank r = (Rank) e.nextElement(); } */ ic.sendLine(); } private static final Paragraph planHeading = new HeadingParagraph( "plan", HeadingParagraph.RIGHT ); public void sendFingerScreen( InteractiveConnection ic ) { boolean seeThrough = permissionList.permissionCheck( seePrivateInfoAction, false, true ); Player p = getCurrent(); ic.sendLine(); ic.send( getTitledName() ); ic.sendLine(); if( connected() ) { ic.send( getFullName() + " has been logged in for " + loginStats.getTimeSinceConnection() ); InteractiveConnection pic = getConnection(); if( pic instanceof SocketIC ) ic.send( getName() + " is connected from: " + ((SocketIC)pic).getFullSiteName() ); } else { DateTime dateTime = (DateTime) loginStats.getLastConnection(); if( dateTime == null ) ic.send( getFullName() + " has never logged in." ); else ic.send( getFullName() + " was last seen at " + dateTime.toString( p ) ); } if( !hideTime ) { ic.send( gender.HisHer() + " total login time is " + loginStats.getTotalConnectionTime() ); if( oldLoginTime != null && oldLoginTime.length() > 0 ) { ic.send( gender.HisHer() + " pre-eclipse login time was " + oldLoginTime ); } } if( age != 0 ) ic.send( gender.HeShe() + " is " + age + " year" + (age==1?"":"s") + " old" ); { if( aka.length() > 0 ) ic.sendFeedback( "Also known as: " + aka ); } // email addresses { if( !privateEmail || seeThrough ) { String ea = getEmailAddress(); if( ea != null ) { ic.sendFeedback( HisHer() + " email address is: " + ea ); } } } { String wp = homepage.get(); if( wp != null ) ic.sendFeedback( HisHer() + " web page is: " + wp ); } Paragraph plan = getPlan(); // show the plan (exile 28jul97) if( plan != null && !plan.isEmpty() ) { ic.send( planHeading ); ic.send( plan ); } ic.sendLine(); } // -- cookies aren't saved, but are kind of cool // -- for whatever you want to use them for. Generally // -- saving state during seperate commands is a good // -- thing. private transient Hashtable cookies; public void setCookie( Object key, Object value ) { if( cookies == null ) cookies = new Hashtable(); cookies.put( key, value ); } public void removeCookie( Object key ) { if( cookies != null ) cookies.remove( key ); } public Object getCookie( Object key ) { if( cookies == null ) return( null ); else return( cookies.get( key ) ); } private transient long[] spamThrottle; private transient int[] spamViolations; private static final int[] THROTTLE_THRESHOLD = { 0, 3 }; private static final int[] THROTTLE_PENALTY = { 1000, 2000 }; public static final int NUMBER_OF_THROTTLES = 2; /** * Called just before a broadcast event. It prevents * spamming broadcast channels. The type is the * 'type' of the channel. Each channel type has different * throttle rates (the more public, generally, the less * tolerant we are of 'spam' type communication. */ public final void aboutToBroadcast( int type ) { long a = System.currentTimeMillis() / 1000; if( (a - THROTTLE_THRESHOLD[type]) <= spamThrottle[type] ) { try { spamViolations[type]++; if( spamViolations[type] > 13 ) { for( int i = 0; i < NUMBER_OF_THROTTLES; i++ ) spamViolations[type] = 0; ic.send( "^hYou have been broadcasting too quickly.^-" ); ic.send( "^hPlease talk a little slower and be more lag.friendly in the future.^-" ); disconnect(); } else { if( spamViolations[type] > 3 ) { Thread.sleep( THROTTLE_PENALTY[type] ); spamThrottle[type]++; } else if( spamViolations[type] > 10 ) { throw new LimitExceededException( "You are sending communication too quickly. Please be lag.friendly and wait a few seconds between says, shouts and tells." ); } } } catch( InterruptedException e ) { } } else { if( a - spamThrottle[type] > 10 ) spamViolations[type] = 0; else { if( spamViolations[type] > 0 ) spamViolations[type]--; } spamThrottle[type] = a; } } public static Player getCurrent() { Thread t = Thread.currentThread(); if( t instanceof Animated ) { Animate a = ((Animated)t).is(); if( a instanceof InteractiveConnection ) { return( ((InteractiveConnection)a).getPlayer() ); } } else { // sometimes, from a system script, currentThread won't be // animated. In this case, however, we're always going // to be the only player online, and Key isn't going to // be running. In that case: Key key = Key.instance(); if( key != null && !key.isRunning() && key.numberPlayers() == 1 ) { for( Enumeration e = key.players(); e.hasMoreElements(); ) { return( (Player) e.nextElement() ); } } } return( null ); } public Object retrieveField( java.lang.reflect.Field f ) throws IllegalAccessException { if( f.getDeclaringClass() == Player.class ) return( f.get( this ) ); else return( super.retrieveField( f ) ); } }