C# Message to Packet converter and assembler... Poke holes in it please :)

Started by
1 comment, last by _winterdyne_ 18 years, 4 months ago
First, lemme preface this by: "This is my first attempt at this, and I have almost no experience working with TCP/IP. Also, this is for TCP, so there are no Acks being sent back (however thats quite possible to implement, rather easily). All messages are sent as Low Priority right now, so they will automatically be wiped out of the buffer when sent. A quick class summary: GameCommand - a basic game command. Has various types {Movement, Social, Slash, Authentication, etc (for now)}, the text of the command, priority, and QueuePacket - A packet that is placed in a queue to be sent. There are 1 to 'n' packets for a GameCommand. CommandQueue - Basically everything having to do with sending or receive packets and the functions that involve building or dissecting them. These contain static arraylists that can be polled from the client and/or server. Here's a logical flow from a server command that executes and sets the client's ID, to be used in all subsequent transactions. No real authentication is in place yet. The server creates a command and then sends it to the player:

//Send them their new player ID
GameCommand objCmd = new GameCommand();
objCmd.Text = "SET_PID:" + ID.ToString();
objCmd.Type = GameCommand.CommandType.Authentication;
objPlayer.SendCommand(objCmd);

The player attaches its own ID to it, so it knows where to go:

public void SendCommand(GameCommand objCommand)
{
	//Tag this with the player ID
	objCommand.ObjectID = ID;

	//Nothing we can really do here, other than dump it to the connection's outbound Queue
	Conn.SendCommandToOutboundQueue(objCommand);
}

The connection then attaches its socket to the command. THis probably isn't necessary in UDP, but We're not into UDP, yet.

public void SendCommandToOutboundQueue(GameCommand objCommand)
{
	objCommand.DestSock = m_TCP;
	CommandQueue.SendToOutBoundQueue(objCommand);
}

The command is then picked up by the CommandQueue object and you can trace it below. Classes:

/// <summary>
/// "Teh!" game command
/// </summary>
public class GameCommand
{

	/// <summary>
	/// The type of command
	/// </summary>
	public CommandType Type;

	/// <summary>
	/// The priority of the current command
	/// </summary>
	public CommandPriority Priority;

	/// <summary>
	/// The actual text of this command
	/// </summary>
	public string Text; 

	/// <summary>
	/// The ID of the 'thing' that this command belongs to
	/// </summary>
	public int ObjectID;

	/// <summary>
	/// The destination socket of the command
	/// </summary>
	public Socket DestSock; 
	
	/// <summary>
	/// Constructor - default command priorty is low
	/// </summary>
	public GameCommand()
	{
		Priority = CommandPriority.Low;	
	}

	/// <summary>
	/// Creates a Command from a single packet
	/// </summary>
	/// <param name="objPacket">The packet containing the current command</param>
	public GameCommand(QueuePacket objPacket)
	{
		this.Type = objPacket.CommandType;
		this.Priority = objPacket.Priority;
		this.Text = objPacket.Message;
		this.ObjectID = (int)objPacket.PacketHeader.ObjectID;
	}

	/// <summary>
	/// Creates a command from a type and text
	/// </summary>
	/// <param name="CType"></param>
	/// <param name="Text"></param>
	public GameCommand(CommandType CType, string Text)
	{
		this.Type = CType;
		this.Text = Text;
		this.Priority = CommandPriority.Low;
	}

	/// <summary>
	/// Returns the text of the command in byte array.
	/// Prettified ;)
	/// </summary>
	/// <returns></returns>
	public byte[] GetBytes()
	{
		
		return System.Text.Encoding.UTF8.GetBytes(Text);
	}

	/// <summary>
	/// Types of command
	/// </summary>
	public enum CommandType
	{
		Movement,
		Social,
		Authentication,
		Slash

	}

	/// <summary>
	/// Priorty levels
	/// </summary>
	public enum CommandPriority
	{
		Low,
		High
	}
}

The Queue Packet Class -

/// <summary>
/// Sealed means its faster!
/// </summary>
public sealed class QueuePacket
{

	/// <summary>
	/// Max length of all packets
	/// </summary>
	public  const int MAXPACKETLENGTH = 17;

	/// <summary>
	/// Size of the header for a packet
	/// </summary>
	public const int HEADERSIZE = 7;

	/// <summary>
	/// Size of the actual data portion of the packet
	/// </summary>
	public const int MAXPACKETDATALENGTH = MAXPACKETLENGTH - HEADERSIZE;

	/// <summary>
	/// Priority of the packet
	/// </summary>
	public GameCommand.CommandPriority Priority;

	/// <summary>
	/// Type of packet
	/// </summary>
	public GameCommand.CommandType CommandType;

	/// <summary>
	/// The packet's header info
	/// </summary>
	public Header PacketHeader;

	/// <summary>
	/// Where this packet is headed to
	/// </summary>
	public Socket DestSock;

	/// <summary>
	/// Default bland-town constructor
	/// </summary>
	public QueuePacket()
	{
		PacketHeader = new Header();
	}

	/// <summary>
	/// The Message in this packet
	/// </summary>
	private byte[] m_Message;

	/// <summary>
	/// The string version of the packet message
	/// </summary>
	public string Message
	{
		set
		{
			m_Message = System.Text.Encoding.UTF8.GetBytes(value);
		}
		get
		{
			return System.Text.Encoding.UTF8.GetString(m_Message);
		}
	}

	/// <summary>
	/// Public property of the packet to return the bytes of the message
	/// </summary>
	public byte[] MessageBytes
	{
		get
		{
			return m_Message;
		}
	}

	/// <summary>
	/// What good is a packet if you can't send it?
	/// </summary>
	/// <returns></returns>
	public bool Send()
	{
		try
		{
			DestSock.Send(ToBytes());
			return true;
		}
		catch
		{
			return false;
		}
			
	}

	/// <summary>
	/// Converts current packet to byte array
	/// </summary>
	/// <returns></returns>

	private byte[] ToBytes()
	{
			
		byte[] objBytes = new byte[MAXPACKETLENGTH];

		objBytes[0] = (byte)Priority;
		objBytes[1] = PacketHeader.ObjectID;
		objBytes[2]	= PacketHeader.MessageID;
		objBytes[3] = PacketHeader.TotalSize;
		objBytes[4] = PacketHeader.PacketLength;
		objBytes[5] = PacketHeader.Offset;
		objBytes[6] = (byte)CommandType;

		for(int i = 0; i < PacketHeader.PacketLength; i++)
		{
			objBytes = m_Message<span style="font-weight:bold;">;
		}

		<span class="cpp-keyword">return</span> objBytes;
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Converts the bytes to a packet object</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="RawBytes"&amp;gt;&amp;lt;/param&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">void</span> LoadFromRaw(byte[] RawBytes)
	{
		

		<span class="cpp-keyword">this</span>.Priority = (GameCommand.CommandPriority)RawBytes[<span class="cpp-number">0</span>];
		<span class="cpp-keyword">this</span>.PacketHeader.ObjectID = RawBytes[<span class="cpp-number">1</span>];
		<span class="cpp-keyword">this</span>.PacketHeader.MessageID = RawBytes[<span class="cpp-number">2</span>];
		<span class="cpp-keyword">this</span>.PacketHeader.TotalSize = RawBytes[<span class="cpp-number">3</span>];
		<span class="cpp-keyword">this</span>.PacketHeader.PacketLength = RawBytes[<span class="cpp-number">4</span>];
		<span class="cpp-keyword">this</span>.PacketHeader.Offset = RawBytes[<span class="cpp-number">5</span>];
		<span class="cpp-keyword">this</span>.CommandType = (GameCommand.CommandType)RawBytes[<span class="cpp-number">6</span>];

		<span class="cpp-keyword">this</span>.Message = System.Text.Encoding.UTF8.GetString(RawBytes, <span class="cpp-number">7</span>, MAXPACKETLENGTH - <span class="cpp-number">7</span>).Trim('\<span class="cpp-number">0</span>');
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Sealed class containging header information</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-keyword">public</span> sealed <span class="cpp-keyword">class</span> Header
	{
		<span class="cpp-keyword">public</span> byte ObjectID;
		<span class="cpp-keyword">public</span> byte MessageID;
		<span class="cpp-keyword">public</span> byte TotalSize;
		<span class="cpp-keyword">public</span> byte PacketLength;
		<span class="cpp-keyword">public</span> byte Offset;
	}
}

</pre></div><!–ENDSCRIPT–>

The command Queue class

<!–STARTSCRIPT–><!–source lang="cpp"–><div class="source"><pre>
<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
<span class="cpp-comment">/// Class that contains all lists for inbound commands and outbound packets.</span>
<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
<span class="cpp-keyword">public</span> <span class="cpp-keyword">class</span> CommandQueue
{
	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Timer that will send all outbound items</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-keyword">private</span> <span class="cpp-keyword">static</span> Timer sendTimer;

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// List containing all outbound packets to be sent</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-keyword">private</span> <span class="cpp-keyword">static</span> ArrayList OutboundPackets = <span class="cpp-keyword">new</span> ArrayList();

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// All commands that have been successfully built</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">static</span> ArrayList IncomingCommands = <span class="cpp-keyword">new</span> ArrayList();

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Fragments of commands that have not been built into commands yet</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">static</span> ArrayList CommandFragments = <span class="cpp-keyword">new</span> ArrayList();

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Starts the timer for sending the Queue</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="SecondsBetweenSends"&amp;gt;How many seconds to wait between sending the Queue&amp;lt;/param&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">void</span> Start(<span class="cpp-keyword">double</span> SecondsBetweenSends)
	{
		sendTimer = <span class="cpp-keyword">new</span> Timer();
		sendTimer.Elapsed += <span class="cpp-keyword">new</span> ElapsedEventHandler(SendQueue);
		sendTimer.Interval = SecondsBetweenSends * <span class="cpp-number">1000</span>;
		sendTimer.Enabled  = <span class="cpp-keyword">true</span>;
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Parses command into packets to be added to outbound Queue</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="objCommand"&amp;gt;&amp;lt;/param&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">void</span> SendToOutBoundQueue(GameCommand objCommand)
	{
		<span class="cpp-comment">//Make sure the MessageQueue is instantiated</span>
		<span class="cpp-keyword">if</span>(OutboundPackets == null)
			OutboundPackets = <span class="cpp-keyword">new</span> ArrayList();

		<span class="cpp-comment">//Make sure this command has somewhere to go</span>
		<span class="cpp-keyword">if</span>(objCommand.DestSock == null)
			<span class="cpp-keyword">throw</span> <span class="cpp-keyword">new</span> Exception(<span class="cpp-literal">"Command in Queue without Destination Socket set"</span>);

		<span class="cpp-comment">//Get the total size of the command</span>
		<span class="cpp-keyword">int</span> intTotalSize = objCommand.Text.Length;

		<span class="cpp-comment">//Set the amount of bytes left</span>
		<span class="cpp-keyword">int</span> intBytesLeft = intTotalSize;

		<span class="cpp-comment">//Set the default offset</span>
		<span class="cpp-keyword">int</span> intOffset = <span class="cpp-number">0</span>;

		<span class="cpp-comment">//Generate a random number for this</span>
		<span class="cpp-keyword">int</span> intMessageID = MiscUtils.GetRandom(<span class="cpp-number">0</span>, <span class="cpp-number">64</span>);

		<span class="cpp-comment">//This is a new packet so the packet len is 0</span>
		<span class="cpp-keyword">int</span> intPacketLen = <span class="cpp-number">0</span>;

		<span class="cpp-comment">//Do this until there is nothing left</span>
		<span class="cpp-keyword">while</span>(intBytesLeft &amp;gt; <span class="cpp-number">0</span>)
		{
			intPacketLen = intBytesLeft;
				
			<span class="cpp-keyword">int</span> intMaxPacketLen = QueuePacket.MAXPACKETDATALENGTH;
				
			<span class="cpp-comment">//Make sure this packet isn't too big to fit</span>
			<span class="cpp-keyword">if</span>(intPacketLen &amp;gt; intMaxPacketLen)
				intPacketLen = intMaxPacketLen;

			<span class="cpp-comment">//Create a new packet class</span>
			QueuePacket objPacket = <span class="cpp-keyword">new</span> QueuePacket();

			objPacket.PacketHeader.MessageID = (byte)intMessageID;
			objPacket.PacketHeader.TotalSize = (byte)intTotalSize;
			objPacket.PacketHeader.Offset = (byte)intOffset;
			objPacket.PacketHeader.PacketLength = (byte)intPacketLen;
				
			objPacket.PacketHeader.ObjectID = (byte)objCommand.ObjectID;

			<span class="cpp-comment">//Set the dest</span>
			objPacket.DestSock = objCommand.DestSock;

			<span class="cpp-comment">//Set command info</span>
			objPacket.Message = objCommand.Text.Substring(intOffset, intPacketLen);
			objPacket.CommandType = objCommand.Type;
			objPacket.Priority = objCommand.Priority;
			
			<span class="cpp-comment">//Add this packet to the outbound queue</span>
			OutboundPackets.Add(objPacket);

			<span class="cpp-comment">//Remove the bytes</span>
			intBytesLeft -= intPacketLen;

			<span class="cpp-comment">//Increment the offset</span>
			intOffset += intPacketLen;
				
		}
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Adds a packet fragment to the Fragment list, and then tries to build a command out of</span>
	<span class="cpp-comment">/// all pieces of the same fragment</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="objPacket"&amp;gt;Fragment to be added to the queue&amp;lt;/param&amp;gt;</span>
	<span class="cpp-keyword">public</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">void</span> AddFragment(QueuePacket objPacket)
	{
		CommandFragments.Add(objPacket);
		BuildCommandFromFragments(objPacket.PacketHeader.MessageID);
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Sends all queued up packets to their destination</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="sender"&amp;gt;Not used&amp;lt;/param&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="e"&amp;gt;Not used&amp;lt;/param&amp;gt;</span>
	<span class="cpp-keyword">private</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">void</span> SendQueue(object sender, ElapsedEventArgs e)
	{
		<span class="cpp-keyword">if</span>(OutboundPackets != null)
		{
			<span class="cpp-keyword">for</span>(<span class="cpp-keyword">int</span> i = <span class="cpp-number">0</span>; i &amp;lt; OutboundPackets.Count; i++)
			{
				QueuePacket objPacket = (QueuePacket)OutboundPackets<span style="font-weight:bold;">;
				objPacket.Send();
				<span class="cpp-comment">//Low priority packets don't need a ack</span>
				<span class="cpp-keyword">if</span>(objPacket.Priority == GameCommand.CommandPriority.Low)
				OutboundPackets.RemoveAt(i);

			}
		}
	}

			
	<span class="cpp-keyword">private</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">void</span> BuildCommandFromFragments(byte MessageID)
	{
			
		<span class="cpp-comment">//Has the first piece arrived? If not, something is wacky and we need to leave</span>
		<span class="cpp-keyword">if</span> (!FindFirstFragment(MessageID))
		{
			<span class="cpp-keyword">return</span>;
		}

				
		<span class="cpp-comment">//This contains all packets with matching MessageID</span>
		ArrayList objFilteredPackets = <span class="cpp-keyword">new</span> ArrayList();

		<span class="cpp-comment">//Pull all matching packets</span>
		foreach(QueuePacket objPacket in CommandFragments)
		{
			<span class="cpp-keyword">if</span>(objPacket.PacketHeader.MessageID == MessageID)
				objFilteredPackets.Add(objPacket);
		}

		<span class="cpp-comment">//If there is just one in here, thats the message that triggered this</span>
		<span class="cpp-comment">//If there is just one packet, it shouldn't be in here anyway.</span>
		<span class="cpp-comment">//Bye</span>
		<span class="cpp-keyword">if</span>(objFilteredPackets.Count &amp;lt;= <span class="cpp-number">1</span>)
			<span class="cpp-keyword">return</span>;

		<span class="cpp-comment">//We have two or more packets now, we can check to see if all pieces are there</span>
		<span class="cpp-keyword">int</span> intTotalPacketLength = ((QueuePacket)objFilteredPackets[<span class="cpp-number">0</span>]).PacketHeader.TotalSize;
		<span class="cpp-keyword">int</span> intTotalPacketsNeeded = <span class="cpp-number">0</span>;

		<span class="cpp-comment">//Determine if there are even-sized packets in this</span>
		<span class="cpp-keyword">if</span>((intTotalPacketLength % QueuePacket.MAXPACKETDATALENGTH) == <span class="cpp-number">0</span>)
		{
			intTotalPacketsNeeded = intTotalPacketLength / QueuePacket.MAXPACKETDATALENGTH;
		}
		<span class="cpp-keyword">else</span>
		{
			intTotalPacketsNeeded = (intTotalPacketLength / QueuePacket.MAXPACKETDATALENGTH) + <span class="cpp-number">1</span>;
		}

		<span class="cpp-comment">//If there arent' enough packets in there, no point in continuing</span>
		<span class="cpp-keyword">if</span>(objFilteredPackets.Count &amp;lt; intTotalPacketsNeeded)
			<span class="cpp-keyword">return</span>;

		<span class="cpp-comment">//TODO: VALIDATE PACKETS HERE</span>
				
		<span class="cpp-comment">//Put packets into a byte array</span>
		byte[] CompleteMessage = <span class="cpp-keyword">new</span> byte[intTotalPacketLength];
			
		foreach(QueuePacket objPacket in objFilteredPackets)
		{
			<span class="cpp-keyword">if</span>(objPacket.PacketHeader.PacketLength != objPacket.Message.Length)
			{
				Debug.WriteLine(<span class="cpp-literal">"Packet Failed Length Assert!"</span>);
			}
			<span class="cpp-keyword">int</span> offset = objPacket.PacketHeader.Offset;
			<span class="cpp-keyword">for</span>(<span class="cpp-keyword">int</span> i = <span class="cpp-number">0</span>; i &amp;lt; objPacket.MessageBytes.Length; i++)
			{
				<span class="cpp-keyword">try</span>
				{
					CompleteMessage[offset + i] = objPacket.MessageBytes<span style="font-weight:bold;">;
				}
				<span class="cpp-keyword">catch</span>
				{
					Debug.WriteLine(<span class="cpp-literal">"Error in Building packets: "</span> + intTotalPacketLength.ToString());
					Debug.WriteLine(<span class="cpp-literal">"Error in Building packets: "</span> + offset.ToString() + <span class="cpp-literal">" - "</span> + i.ToString() + <span class="cpp-literal">" – "</span>  + objPacket.Message);
				}
			}
		}

		<span class="cpp-comment">//Whee, done building!</span>
		<span class="cpp-comment">//Create a game command and add it to the incoming command buffer</span>
		GameCommand objCommand = <span class="cpp-keyword">new</span> GameCommand();

		objCommand.Type = ((QueuePacket)objFilteredPackets[<span class="cpp-number">0</span>]).CommandType;
		objCommand.Priority = ((QueuePacket)objFilteredPackets[<span class="cpp-number">0</span>]).Priority;
		objCommand.Text = System.Text.Encoding.UTF8.GetString(CompleteMessage);
		objCommand.ObjectID = ((QueuePacket)objFilteredPackets[<span class="cpp-number">0</span>]).PacketHeader.ObjectID;

		IncomingCommands.Add(objCommand);
			
	}

	<span class="cpp-comment">/// &amp;lt;summary&amp;gt;</span>
	<span class="cpp-comment">/// Returns a true or false if the initial portion of the current message has been</span>
	<span class="cpp-comment">/// received and stored as a fragment</span>
	<span class="cpp-comment">/// &amp;lt;/summary&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;param name="MessageID"&amp;gt;ID of the first packet to look for&amp;lt;/param&amp;gt;</span>
	<span class="cpp-comment">/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;</span>
	<span class="cpp-keyword">private</span> <span class="cpp-keyword">static</span> <span class="cpp-keyword">bool</span> FindFirstFragment(byte MessageID)
	{
		foreach(QueuePacket objPacket in CommandFragments)
		{
			<span class="cpp-keyword">if</span>(objPacket.PacketHeader.MessageID == MessageID &amp;&amp; objPacket.PacketHeader.Offset == <span class="cpp-number">0</span>)
				<span class="cpp-keyword">return</span> <span class="cpp-keyword">true</span>;
		}
		<span class="cpp-keyword">return</span> <span class="cpp-keyword">false</span>;
	}
}

</pre></div><!–ENDSCRIPT–>


This whole process really doesn't generate any overhead for the server, so it might be feasible to scale up.  However, I'm not sure, and would love to have some feedback :D

Thanks in advance, and sorry about the long post.

PS:  Special thanks to Winter, as I mainly modified some of his C code to C#.  I hate pointers :D



Edit: put the code in "source" tags so the long lines don't ruin the regular text.

<!–EDIT–><span class=editedby><!–/EDIT–>[Edited by - hplus0603 on November 27, 2005 8:56:07 PM]<!–EDIT–></span><!–/EDIT–>
Advertisement
Bump and thanks for the help on the source tags. Should have that on the left side, like most other boards, but now I know!


Anyone have any ideas on whether this looks good or not?
Don't thank me - the code I put up was refactored from Planeshift.

It seems ok to me from skim-reading it, but I would point out one thing - you're referring to your message as a 'command'. This is bad practice - your network message might not be a command, and if you get yourself into the mindset that all your netcode is for is passing that command you'll find it tricky to spot bugs. Just a psychological thing, but I do recommend you call a message a message. :-) You'll need to validate that it IS a command before you can call it that.
Winterdyne Solutions Ltd is recruiting - this thread for details!

This topic is closed to new replies.

Advertisement