#region Arthea License /*********************************************************************** * Arthea MUD by R. Jennings (2007) http://arthea.googlecode.com/ * * By using this code you comply with the Artistic and GPLv2 Licenses. * ***********************************************************************/ #endregion using System; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Text; using Arthea.Clans; using Arthea.Classes; using Arthea.Commands; using Arthea.Commands.Admin; using Arthea.Connections.Colors; using Arthea.Connections.Enums; using Arthea.Connections.Players; using Arthea.Creation; using Arthea.Environment; using Arthea.Races; using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; namespace Arthea.Connections { /// <summary> /// Implements a connection to the server /// </summary> public class Connection { #region [rgn] Fields (16) private StringEditor composing; private Olc editing; private ConnectionFlags flags = new ConnectionFlags(); private bool foundCommand; private StringBuilder inBuf = new StringBuilder(); private Deflater mccp; private StringBuilder outBuf = new StringBuilder(); private Player player; private int screenHeight = 24; private int screenWidth = 80; private bool sentPrompt; private Socket socket; private ConnectionState state = ConnectionState.EnterName; private string systemType; private string terminalType; private int wait; #endregion [rgn] #region [rgn] Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Connection"/> class. /// </summary> /// <param name="name">The name.</param> /// <param name="info">The info.</param> public Connection(string name, SocketInformation info) { socket = new Socket(info); Player = Player.Load(name); State = ConnectionState.Playing; } /// <summary> /// Initializes a new instance of the <see cref="Connection"/> class. /// </summary> /// <param name="socket">The socket.</param> public Connection(Socket socket) { this.socket = socket; OnConnect(); } #endregion [rgn] #region [rgn] Properties (13) /// <summary> /// Gets or sets the string being composed. /// </summary> /// <value>The composing.</value> public StringEditor Composing { get { return composing; } set { if (value != null) { composing = value; WriteLine("You are new entering the text editor. Type '/?' on a new line for help."); } else { if (composing != null) { WriteLine("You stop using the text editor."); } composing = value; } } } /// <summary> /// Gets a value indicating whether this <see cref="Connection"/> is connected. /// </summary> /// <value><c>true</c> if connected; otherwise, <c>false</c>.</value> public bool Connected { get { return (socket != null && socket.Connected); } } /// <summary> /// Gets or sets the editing. /// </summary> /// <value>The editing.</value> public Olc Editing { get { return editing; } set { if (value != null) { editing = value; WriteLine("You are now entering the {0} editor. Type '?' for help.", Olc.FieldToWords(editing.GetType().Name).ToLower()); } else { if (editing != null) { WriteLine("You stop using the {0} editor.", Olc.FieldToWords(editing.GetType().Name).ToLower()); } editing = value; } } } /// <summary> /// Gets or sets the flags /// </summary> /// <value>the connection flags</value> public ConnectionFlags Flags { get { return flags; } set { flags = value; } } /// <summary> /// Gets or sets the player. /// </summary> /// <value>The player.</value> public Player Player { get { return player; } set { if (player != null && value != player) { player.Connection = null; } player = value; if (player != null) { player.Connection = this; } } } /// <summary> /// Gets or sets the screen height /// </summary> /// <value>the screen height</value> public int ScreenHeight { get { return screenHeight; } set { screenHeight = value; } } /// <summary> /// Gets or sets the screen width /// </summary> /// <value>the screen width</value> public int ScreenWidth { get { return screenWidth; } set { screenWidth = value; } } /// <summary> /// Gets or sets a value indicating whether [prompt was sent]. /// </summary> /// <value><c>true</c> if [prompt was sent]; otherwise, <c>false</c>.</value> public bool SentPrompt { get { return sentPrompt; } set { sentPrompt = value; } } /// <summary> /// Gets or sets the socket. /// </summary> /// <value>The socket.</value> public Socket Socket { get { return socket; } set { socket = value; } } /// <summary> /// Gets or sets the state. /// </summary> /// <value>The state.</value> public ConnectionState State { get { return state; } set { state = value; PromptConnectionState(); } } /// <summary> /// Gets or sets the type of the system. /// </summary> /// <value>The type of the system.</value> public string SystemType { get { return systemType; } set { systemType = value; } } /// <summary> /// Gets or sets the type of the terminal. /// </summary> /// <value>The type of the terminal.</value> public string TerminalType { get { return terminalType; } set { terminalType = value; } } /// <summary> /// Gets or sets the wait time. /// </summary> /// <value>The wait.</value> public int Wait { get { return wait; } set { wait = value; } } #endregion [rgn] #region [rgn] Methods (25) // [rgn] Public Methods (20) /// <summary> /// Attaches this instance to applicable lists. /// </summary> public void Attach() { Lists.Connections.Add(this); } /// <summary> /// Determines whether this instance can error. /// </summary> /// <returns> /// <c>true</c> if this instance can error; otherwise, <c>false</c>. /// </returns> public bool CanError() { return socket.Poll(1, SelectMode.SelectError); } /// <summary> /// Determines whether this instance can read. /// </summary> /// <returns> /// <c>true</c> if this instance can read; otherwise, <c>false</c>. /// </returns> public bool CanRead() { return socket.Available > 0; } /// <summary> /// Determines whether this instance can write. /// </summary> /// <returns> /// <c>true</c> if this instance can write; otherwise, <c>false</c>. /// </returns> public bool CanWrite() { return socket.Poll(1, SelectMode.SelectWrite) && outBuf.ToString().Length > 0; } /// <summary> /// Closes this instance. /// </summary> public void Close() { Release(); Socket.Close(); } /// <summary> /// Edits the specified obj. /// </summary> /// <param name="obj">The obj.</param> public void Edit(object obj) { if (obj != null) Editing = new Olc(this, obj); } /// <summary> /// Determines whether this instance has input. /// </summary> /// <returns> /// <c>true</c> if this instance has input; otherwise, <c>false</c>. /// </returns> public bool HasInput() { return String.HasLine(inBuf.ToString()); } /// <summary> /// Checks for an invalid name. /// </summary> /// <param name="name">The name.</param> /// <returns></returns> public bool InvalidName(String name) { foreach (char c in name) { if (!char.IsLetter(c)) { WriteLine("Invalid name. Please use only letters in your name."); return true; } } if (name.Length > 12 || name.Length < 2) { WriteLine("Invalid name. Please choose a name between 2 and 12 characters."); return true; } foreach (Clan clan in Lists.Clans) { if (name == clan.Name) { WriteLine("There is a clan with that name already."); return true; } } return false; } /// <summary> /// Process a connections input /// </summary> public void ProcessInput() { Debug.Assert(inBuf.Length > 0); String buf = inBuf.ToString(); String line = buf.GetLine().Trim(); inBuf = new StringBuilder(buf); foundCommand = true; try { StateHandler(line); } catch (Exception ex) { Log.Bug(ex.Message); Log.Error(ex.StackTrace); } finally { foundCommand = false; } } /// <summary> /// Processes the output. /// </summary> public void ProcessOutput() { if (State == ConnectionState.Playing && Player != null && !sentPrompt) { Player.DisplayPrompt(); } string text = Color.Convert(outBuf.ToString(), Player); WriteToSocket(Globals.Encoding.GetBytes(text)); outBuf = new StringBuilder(); } /// <summary> /// Reads input. /// </summary> public void Read() { byte[] bytes = new byte[Socket.Available]; int nRead = Socket.Receive(bytes); for (int i = 0; i < nRead; i++) { // handle delete and backspace if (bytes[i] == 127 || bytes[i] == Telopt.Backspace) { if (inBuf.Length > 0) inBuf.Remove(inBuf.Length - 1, 1); continue; } else if (bytes[i] == Telopt.IAC) { Telnet.Process(bytes, ref i, this); continue; } char c = Convert.ToChar(bytes[i]); inBuf.Append(c); // cancel composite format characters switch (c) { case '}': inBuf.Append('}'); break; case '{': inBuf.Append('{'); break; } } } /// <summary> /// Releases this instance from applicable lists. /// </summary> public void Release() { Lists.Connections.Remove(this); if (Player != null) { Player.Release(); Player = null; } Editing = null; Composing = null; } /// <summary> /// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>. /// </summary> /// <returns> /// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>. /// </returns> public override string ToString() { if (socket == null) return "disconnected"; return socket.ToString(); } // [rgn] Private Methods (5) private void Newbize() { WriteLine("Welcome to Arthea, {0}, enjoy your stay.", player.Name); WriteLine("Type 'help' for assistance."); WriteLine(); Player.Title = string.Format("the {0} {1}.", Player.Race.ToString().ToLower(), Player.Class.ToString().ToLower()); if (Persistance.GetDirectoryXmlFiles(Paths.PlayerDir).Length == 0) { Player.Level = Levels.Admin; WriteLine("You are the first connection, granting administrator privledges."); foreach (Command cmd in Lists.Commands.FindAdmin()) Player.Powers.Add(cmd); foreach (Powers power in Enum.GetValues(typeof (Powers))) Player.Powers.Add(power); Log.Warn("No players, setting {0} as an admin.", Player.Name); } else { Player.Level = 1; WriteLine("You are now level 1."); } Player.Room = Lists.Rooms[Globals.Limbo]; WriteLine(); } /// <summary> /// Prefixes a new line a number of times. /// Positive values remains prefixing 1 line consecutivly. /// Negative values stop prefixing after initial prefixes. /// </summary> private void NewLinePrefix() { if (!foundCommand) outBuf.AppendLine(); if (sentPrompt) { outBuf.AppendLine(); sentPrompt = false; } } /// <summary> /// Called when [connect]. /// </summary> private void OnConnect() { // Let the client request options - RJ Sep'07 // Telnet.Advertise(this); if (File.Exists(Paths.GreetingFile)) { WriteLine(File.ReadAllText(Paths.GreetingFile)); } else { WriteLine("Welcome to..."); Write(Server.Instance.Version); WriteLine(Server.Instance.About); } if (!Globals.LockDown) { Write("What is your name? "); Attach(); } else { WriteLine("Arthea is under a lock down. No new connections are being accepted."); Wait = 100; State = ConnectionState.Disconnected; } } /// <summary> /// Starts the connections player playing. /// </summary> private void SetPlaying() { if (Player == null) return; Lists.Characters.Add(Player); Lists.Players.Add(Player); LookCommand.Instance.Process(Player, ""); Player.Act(null, null, Act.ToWorld, "$n has returned to Arthea."); } /// <summary> /// Handles the connections state. /// </summary> /// <param name="input">The input of input.</param> private void StateHandler(String input) { if (Composing != null) { Composing.Compose(input); return; } if (Editing != null && Editing.Edit(input)) { return; } switch (state) { case ConnectionState.Playing: HandlePlayingState(input); break; case ConnectionState.EnterName: HandleEnterName(input); break; case ConnectionState.ConfirmNewName: HandleNewName(input); break; case ConnectionState.EnterPassword: HandleEnterPassword(input); break; case ConnectionState.ConfirmReconnect: HandleConfirmReconnect(input); break; case ConnectionState.EnterNewPassword: HandleEnterNewPassword(input); break; case ConnectionState.ConfirmNewPassword: HandleConfirmNewPassword(input); break; case ConnectionState.RollStats: HandleRollStats(input); break; case ConnectionState.GetClass: HandleGetClass(input); break; case ConnectionState.GetRace: HandleGetRace(input); break; case ConnectionState.ReadMOTD: HandleReadMOTD(); break; } } #region WriteMethods /// <summary> /// Writes the specified text. /// </summary> /// <param name="text">The text.</param> /// <param name="args">The args.</param> public void Write(string text, params object[] args) { NewLinePrefix(); outBuf.AppendFormat(text, args); } /// <summary> /// Writes the input. /// </summary> /// <param name="text">The text.</param> /// <param name="args">The args.</param> public void WriteLine(string text, params object[] args) { NewLinePrefix(); outBuf.AppendFormat(text, args); outBuf.AppendLine(); } /// <summary> /// Writes a input. /// </summary> public void WriteLine() { outBuf.AppendLine(); } /// <summary> /// Writes raw bytes to the socket. /// </summary> /// <param name="text">The bytes.</param> public void WriteToSocket(byte[] text) { #if ENABLE_COMPRESSION if (flags.Has(ConnectionFlags.MCCP)) { Socket.Send(Compress(text)); return; } #endif Socket.Send(text); } #endregion #region Compression Methods /// <summary> /// Compresses the specified bytes. /// Theoretically, this implementation of MCCP /// should work, but I can't get it to. /// </summary> /// <param name="text">The text.</param> /// <returns>compressed bytes</returns> public byte[] Compress(byte[] text) { MemoryStream mem = new MemoryStream(); DeflaterOutputStream s = new DeflaterOutputStream(mem, mccp); s.Write(text, 0, text.Length); s.Finish(); s.Flush(); s.Close(); return mem.ToArray(); } /// <summary> /// Ends the compression. /// </summary> public void EndCompression() { flags.Remove(ConnectionFlags.MCCP); WriteLine("Compressed ratio was {0}%.", (mccp.TotalIn*100)/mccp.TotalOut); mccp = null; } /// <summary> /// Starts the compression. /// </summary> public void StartCompression() { flags.Set(ConnectionFlags.MCCP); mccp = new Deflater(Deflater.BEST_COMPRESSION, false); } #endregion #endregion [rgn] #region ConnectionState Handlers private void PromptConnectionState() { switch (state) { case ConnectionState.Playing: SetPlaying(); break; case ConnectionState.ReadMOTD: if (File.Exists(Paths.MOTDFile)) { WriteLine(File.ReadAllText(Paths.MOTDFile)); } WriteLine("[Hit enter to continue]"); break; case ConnectionState.ConfirmReconnect: Write("Would to like to reconnect (Y/N)? "); break; case ConnectionState.EnterPassword: Write("Please enter your password: "); WriteToSocket(Telnet.EchoOff); break; case ConnectionState.ConfirmNewName: Write("Are you sure you want to be known as {0}? ", Player.Name); break; case ConnectionState.EnterNewPassword: Write("Please enter a password: "); WriteToSocket(Telnet.EchoOff); break; case ConnectionState.EnterName: Write("Ok then, what is your name? "); break; case ConnectionState.ConfirmNewPassword: Write("Enter password again: "); break; case ConnectionState.RollStats: Player.Stats.Roll(); WriteLine(); Player.WriteLine(Player.Stats); WriteLine(); Write("Accept stats (Y/n)? "); break; case ConnectionState.GetRace: WriteLine(); WriteLine(Lists.Races.ToString()); Write("What is your race? "); break; case ConnectionState.GetClass: WriteLine(); WriteLine(Lists.Classes.ToString()); Write("What is you class? (help <class> for more info): "); break; } } /// <summary> /// Handles the playing state. /// </summary> /// <param name="input">The input of input.</param> private void HandlePlayingState(String input) { try { Command.Interpret(Player, input); } catch (Exception ex) { Player.WriteLine("There was an error processing your command."); Log.Bug(ex.Message); Log.Error(ex.StackTrace); } } /// <summary> /// Handles entering a name. /// </summary> /// <param name="input">The input.</param> private void HandleEnterName(String input) { input = input.Capitalize(); Player twin = Lists.Players.FindName(input); if (twin != null) { WriteLine("There is already a player named '{0}' playing.", twin.Name); if (twin.Connection == null) { State = ConnectionState.ConfirmReconnect; } else { WriteLine(); Write("What is your name? "); } return; } if (Persistance.XmlFileExists(Paths.PlayerDir + input)) { try { Player = Player.Load(input.ToString()); State = ConnectionState.EnterPassword; } catch (Exception ex) { Log.Bug(ex.Message); Log.Error(ex.StackTrace); WriteLine("There was a problem loading that player file."); WriteLine("Please send an email to the administrators to fix the problem."); WriteLine(); Write("You may enter another name: "); Player = null; } } else if (!InvalidName(input)) { Player = new Player(this, input); State = ConnectionState.ConfirmNewName; } else { WriteLine(); Write("Please enter another name: "); } } /// <summary> /// Handles a new name. /// </summary> /// <param name="input">The input.</param> private void HandleNewName(String input) { switch (char.ToUpper(input[0])) { case 'Y': WriteLine("Welcome to Arthea {0}.", Player.Name); State = ConnectionState.EnterNewPassword; break; case 'N': State = ConnectionState.EnterName; break; default: Write("Please enter Yes or No: "); break; } } /// <summary> /// Handles entering a password. /// </summary> /// <param name="input">The input.</param> private void HandleEnterPassword(String input) { WriteLine(); if (Util.Encrypt(input) == Player.Password) { Player.Connection = this; Log.Info("{0} has returned.", Player.Name); WriteToSocket(Telnet.EchoOn); State = ConnectionState.ReadMOTD; } else { WriteLine("Invalid password."); WriteLine(); Write("Enter your password: "); } } /// <summary> /// Handles confirming a reconnect. /// </summary> /// <param name="input">The input.</param> private void HandleConfirmReconnect(String input) { switch (char.ToUpper(input[0])) { case 'Y': State = ConnectionState.EnterPassword; break; case 'N': State = ConnectionState.EnterName; Player = null; break; default: WriteLine(); Write("Would you like to reconnect to {0} (Y/N)?", Player.Name); break; } } /// <summary> /// Handles entering a new password. /// </summary> /// <param name="input">The input.</param> private void HandleEnterNewPassword(String input) { if (input.Length > 12 || input.Length < 6) { WriteLine("Invalid password. Please choose a password between 6 and 12 characters."); WriteLine(); Write("Enter new password: "); return; } Player.Password = Util.Encrypt(input.ToString()); State = ConnectionState.ConfirmNewPassword; } /// <summary> /// Handles confirming a new password. /// </summary> /// <param name="input">The input.</param> private void HandleConfirmNewPassword(String input) { if (Util.Encrypt(input.ToString()) == Player.Password) { Log.Info("New player '{0}' has connected.", Player.Name); WriteToSocket(Telnet.EchoOn); State = ConnectionState.RollStats; } else { WriteLine("Passwords don't match."); State = ConnectionState.EnterNewPassword; } } private void HandleRollStats(String input) { switch (char.ToUpper(input[0])) { case 'Y': State = ConnectionState.GetRace; break; case 'N': State = ConnectionState.RollStats; break; default: Player.Write("Please enter (Y)es or (N)o: "); break; } } private void HandleGetRace(String input) { Race race; if (input.StartsWith("help")) { input.FirstArg(); race = Lists.Races.FindPlayerRaceName(input); if (race == null) { WriteLine("That is not a race."); } else { WriteLine(race.ToString()); } Write("What is your race? "); return; } race = Lists.Races.FindPlayerRaceName(input); if (race == null) { Write("That is not a race. What is your race? "); return; } Player.Race = race; WriteLine("You are now a {0}.", race.Name); State = ConnectionState.GetClass; } /// <summary> /// Handles the getting the players class. /// </summary> /// <param name="input">The input.</param> private void HandleGetClass(String input) { Class @class; if (input == "help") { input.FirstArg(); @class = Lists.Classes.FindName(input); if (@class == null) { WriteLine("No such class."); } else { WriteLine(@class.ToString()); } Write("What is your class? "); return; } @class = Lists.Classes.FindName(input); if (@class == null) { Write("That is not a class. What is your class? "); return; } Player.Class = @class; WriteLine("Ok, you are a {0}.", @class.Name); WriteLine(); State = ConnectionState.ReadMOTD; } /// <summary> /// Handles the read MOTD. /// </summary> private void HandleReadMOTD() { if (Player.Level == 0) { Newbize(); } else { WriteLine("Welcome back {0}!", Player.Name); WriteLine(); } State = ConnectionState.Playing; } #endregion } }