#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
}
}