Game Development Community

Rel: Custom protocol chat server written in TorqueScript

by John Vanderbeck · in Torque Game Engine · 03/29/2004 (5:40 am) · 13 replies

Original thread for reference.

This is really quite cool, and I learned a lot about Torque doing this. I also have a new respect for TorqueScript. You see, this is a completely custom chat server, using a custom protocol and I was able to write it 100% using TorqueScript. That just rocks!

Anyway, i've cleaned up the code in the Simple Chat Server and commented the hell out of it. I'm posting it here so that others can learn from it as I did.

Disclaimer: First the standard crap. This runs on my system, it may not run on yours. If you're computer breaks, phases into another dimension, or anything else happnes that you dont' like, you won't blame me. Also bear in mind that this is super super simple. It would never fly as a production application. This is for learning and demonstration purposes only.

Right now i'm only posting the server. I'll see about posting the client as well soo, but really everything there is to learn is here in the server. The client is just a subset of the server code with a GUI on top of it. In any case, I will release the client, but it will take me longer to clean up. I really want to restructure the entire client code to make it clearer.

#1
03/29/2004 (5:40 am)
// We use a package to override the stock onStart() and onExit() functions with our own customized version.
package mayhemChatServer
{
function onStart()
{
   Parent::onStart();
   echo("\nMayhem 2090 Chat Server starting up...");

   initChatServer();
}
function onExit()
{
   echo("\nMayhem 2090 Chat Server shutting down...");

//   echo("Exporting server prefs");
//   export("$Pref::Server::*", "./server/prefs.cs", False);

   Parent::onExit();
}

}; // end package

// Activate our custom package, which in turn loads our custom versions
activatePackage(mayhemChatServer);

// We need a way to track clients connected to the chat server and cross reference them with
// nicknames and what not.
// We use a 2D array to store this information.  We reuse the empty slots as clients log on and off
$clients[0,"clientID"] = 0;					// the %this pointer to the TCPObject that is thier connection
$clients[0,"clientNickname"] = "Server";	// the client's chat nickname
$clients[0,"connected"] = true;				// are they currently connected?


// Start the whole ball of wax
function initChatServer()
{
   trace(false);				// turn this on to trace calls for debugging

   // enable a "console" inside the DOS box
   enableWinConsole(true);		
   
   // create a new TCPObject
   // This object will be our main "server" connection.  It will sit and listen
   // for incomming connections, and then accept them as they come in
   // spawning off new client connections for each.
   // This TCPObject will remains as it is throughout the server's life.
   new TCPObject(chatConnection);
   
   // Tell our TCPObject to listen for connections on port 20010
   chatConnection.listen(20010);
   echo("Listening for new connections on port 20010");

   // This is simply a periodic check to see if we are connected to any clients or not
   // If it sees we have no active connections, it terminates the server.
   // Again, this is just a real simple setup for testing.  
   // To be honest i'm not even sure why I have this.  I guess I put it in originally
   // as just some method so that the app wouldn't exit right away.
   $PollTime = 60; // time, in minutes, between checks to see if the server is connected to any clients
   chatConnection.schedule($PollTime * 60 * 1000, "checkOnline");   
   echo("Sechedule set");
}

function chatConnection::onConnectRequest(%this, %address, %id)
{
	// In this callback we have been told that a client is trying to connect
	// The underlying net code will automaticly accept the connection and spawn a new socket
	// for us.  In here we need to create a new TCPObject to handle this connection
	// and "bind" it to the new socket.  The new socket is whats passed in through %id.
	// To "bind" our new TCPObject to this socket we use a feature of the engine
	// which allows you to create a new object with "arguments" in a similiar fashion
	// to command line arguments when starting the application.
	
	// create a new TCPObject, and pass the new socket ID in as an argument
	%client = new TCPObject(chatClient, %id);
	
	// Record our new client
	// Notice that we save the new TCPObject in the clientID slot.  This allows us to not
	// only cross reference later, but to use it to send and receive for that client.
	// We can actually make calls off it.  $clients[%i,"clientID"].function() is a valid call.
	%i = findOpenClientSlot();
	$clients[%i,"clientID"] = %client;
	$clients[%i,"clientNickname"] = "Guest";
	$clients[%i,"connected"] = true;
}
#2
03/29/2004 (5:42 am)
//***************************************************************
//************** Chat Client Functions **************************
// chatClient is a "spawned" TCPObject that is created once
// a connection is made to a client.  The original chatConnection
// object remains as it was to accept new connections.
// All client work is done through chatClient.
//***************************************************************

// onDisconnect is called when the connection is disconnected.
// There is no distinction on how or who disconnected it.  This gets
// called in all situations.
function chatClient::onDisconnect(%this)
{
	// find client record
	echo("Searching for client record...");
	%i = findClientRecord(%this);
	// if we found a valid client record...
	if (%i > 0)
	{
   	   echo("Found client record for '" @ $clients[%i,"clientNickname"] @ "'");	
   	   // clear this record so we can re-use it
	   clearClientRecord(%i);
	   echo("Client record cleared\nClient disconnected properly.");
	}
	else
	{
		// We weren't able to find this client record, so we probably have
		// a client slot filled but unused now.
		echo("Unable to locate client record\nClient may not be properly disconnected");
	}
}

// This function is called whenever data is received by the TCPObject. 
// TCPObject sends/receives everything as lines of text terminated
// by a newline character.
function chatClient::onLine(%this, %line)
{
	// just take the ine of text and feed it into our processor
	chatClient.processLine(%line, %this);
}

// This function basically just checks to see if anyone is connected
// and if we are not connected it quits the application.  If we are
// connected, it simply resets the schedule to check again in a little bit.
function chatConnection::checkOnline(%this)
{
   echo("checkOnline()");
   
   // See how many active connections we have
   if (getConnectionCount() == 0)
      quit();
   else
      chatConnection.schedule($PollTime * 60 * 1000, "checkOnline");   
}
#3
03/29/2004 (5:42 am)
function chatClient::processLine(%this, %line, %id)
{
	// Here we want to process the text we received.  We recognize commands, etc and 
	// do what we need to do with them.
	
	// We need to establish a basic protocol for our chat.  Right now we're writing this
	// as a sort of demo/resource, so we're going to use something dead simple.
	// It will be expanded later for Mayhem's use.
	
	// Simple Chat Protocol
	// v1.0
	// [command]:[argument]
	// command = command is a simple one word command that instructs us what we need to do
	// argument = argument is a string of text that goes with command.
	//
	// There will never be more than one argument per command.
	//
	// login:username
	// goodbye:quit message
	// send:chat message
	// emote:emote message
	// nick:nickname
	
	// first we split the line up into two parts.  The first part is the command
	// and the second part is the argument.
	// We search for our seperator from the BEGINNING of the line, because its possible our argument
	// might have a seperator character in it.  The command never will, so the FIRST one found
	// will always be the right one unless its a malformed command.  If it is, that will be dealt with.
	%posSep = strpos(%line, ":");
	if (%posSep == 0)
		return;
	%len = strlen(%line);
	%command = getSubStr(%line, 0, %posSep);
	%argument = getSubStr(%line, %posSep + 1, %len - %posSep + 1);

	// get user details
	%nickname = getClientNickname(%id);
    %i = findClientRecord(%id);

	// This is the string form of the switch/case command
    switch$ (%command)
	{
		//*****************************************************************************************   
		case "login":
		   // the user is logging in for the first time, so we need to set his nickname
		   $clients[%i,"clientNickname"] = %argument;
		   
		   // send a chat message notifying everyone of the login
		   sendChat("public", "server", "", $clients[%i,"clientNickname"] @ " has signed on.");
		//*****************************************************************************************   
		case "goodbye":
		   // user has requested to log off.  Send his goodbye message, then log him off.
		   sendChat("public", "server", "", %argument);
		   $clients[%i,"clientID"].disconnect();
		//*****************************************************************************************   
		case "send":
		   // A simple chat message.  We just pass this straight through to the send chat methods
		   sendChat("public", %nickname, "all", %argument);
		//*****************************************************************************************   
		case "emote":
		   // This is a request to display an emote.  Not currently supported.
		   echo("[" @ %nickname @ ":" @ %id @"] Command 'emote' has been issued");
		//*****************************************************************************************   
		case "nick":
		   // The user wants to change his nickname.  There is absolutely no checking here to
		   // prevent him from changing it to someone elses.  Again this is all simple and
		   // instructional.
		   
		   // Set his new nickname
		   $clients[%i,"clientNickname"] = %argument;
		   // notify everyone of the change
		   sendChat("public", "server", "", %nickname @ "is now known as " @ %argument);
		//*****************************************************************************************   
		default:
		   // If the switch gets to here, then we have a bad or unknown command.
		   echo("[" @ %nickname @ ":" @ %id @"] Malformed command, or unrecognized command has been issued");
	}	
}
#4
03/29/2004 (5:43 am)
// This function is a router and formatter.  We tell it what we want to send
// as well as what type of message it is, who sent it, and who it is sent to, and 
// it will format it appropriately and send it on down to be sent out.
function sendChat(%type, %from, %to, %message)
{
	// If the message type is public...
	if (%type == "public")
	{
		// The message needs to look like it came from the server
		// In otherwords, a system message
		if (%from $= "server")
		{
			// if the server itself is sending the message, then it gets formatted differently.
			sendChatToAll("send:<Font:Arial Bold:16><Color:00AA00>" @ %message);
		}
		// This message is from a user
		else
		{
			// A general chat message sent to everyone
			if (%to $= "all")
				sendChatToAll("send:" @ %from @ " says: " @ %message);
		}
	}
}

// This function takes a formatted chat message and simply sends it out to
// every client it knows about.
function sendChatToAll(%message)
{
	// we have to iterate through every client and send this to them.
	// we start at index 1 because index 0 is our server.
	%i = 1;
	while ($clients[%i, "clientID"] != 0)
	{
		$clients[%i, "clientID"].send(%message @ "\n");
		%i++;
	}
	// we also echo this message to the server
	
	// This chunk of code just does the same thing it does in
	// the process line function.  Splits the command from the argument.
	// We have to do this here because our message has already been formatted with the
	// command included.
	%posSep = strpos(%message, ":");
	if (%posSep == 0)
		return;
	%len = strlen(%message);
	%command = getSubStr(%message, 0, %posSep);
	%argument = getSubStr(%message, %posSep + 1, %len - %posSep + 1);
	// echo the message
	echo(%argument);
}

// This is essentially the same as the above function except instead of sending
// the chat message to all clients, we only send it to a specific client.
// This would be used for private messages.
function sendChatToClient(%client, %message)
{
	// send this to just a specific client
	%i = findClientRecord(%client);
	$clients[%i,"clientID"].send(%message);
	
	// We do not echo it, because its assumed to be a private message.
}

// Returns total number of active connections, minus the main server connection
// Essentially this just iterates through our clients.
// This function is _FLAWED_ however so dont' rely on it.  Replace this for anything real.
// If you have 3 clients connect, filling indices 1,2, and 3 and then client in index
// 2 logs off, this function when it iterates would stop at index 2, giving a flawed result.
function getConnectionCount()
{
	%connectionCount = 0;
	
	%i = 0;
	while ($clients[%i,"clientNickname"] !$= "")
	{
		if ($clients[%i,"connected"])
			%connectionCount++;
			
		%i++;
	}	
	
	return %connectionCount - 1;
}

// This just iterates through to find an empty client record so we can use it.
// This is what allows us to re-use client records.
function findOpenClientSlot()
{
	%i = 0;
	while ($clients[%i,"clientNickname"] !$="")
	{
		%i++;
	}
	
	return %i;
}
// Given a TCPObject pointer, this function finds the client record associated with it.
// This is a small unseen function, but without it this entire application would not work.
// This is how we are able to tell what client each event is for.  Without it, this would be
// an undoable application.
function findClientRecord(%id)
{
	%i = 0;
	while ($clients[%i,"clientID"] != %id)
	{
		%i++;
	}
	
	return %i;
}
// This function just clears out the client record so it can be re-used.
function clearClientRecord(%id)
{
	$clients[%id,"clientID"] = 0;
	$clients[%id,"clientNickname"] = "";
	$clients[%id,"connected"] = false;
}
// Given a TCPObject pointer, return the nickname of the client.
function getClientNickname(%id)
{
	%i = findClientRecord(%id);
	
	return $clients[%i,"clientNickname"];
}
#5
03/29/2004 (5:44 am)
Enjoy!

Dont' hesitate to ask for any clarifications or whatever.

Feel free to use this code for whatever the heck you want, all I ask is credit and that you tell me what you did with it.
#6
03/29/2004 (5:50 am)
You should post this as a resource. Very cool, btw!

- Brett
#7
03/29/2004 (5:52 am)
I wanted to get the server code out there for people. I'll post it as a resource once i've cleanedup the client code. That's going to take me a while to do though. might not be today, so I wanted to at least get this out. The server is the meat of the whole thing.
#8
03/29/2004 (5:55 am)
Very cool.. Looking forward to the rest.

Ben
#9
03/29/2004 (12:31 pm)
Very nice, John!
#10
03/29/2004 (12:59 pm)
Davis, go grab a napkin.. drooling is not very professional ;)
#11
03/29/2004 (2:27 pm)
Impressive!
#12
03/29/2004 (2:37 pm)
Thanks for the nice comments guys. It really means a lot to me :)
#13
04/06/2004 (8:21 am)
As a reference for archival purposes, this is now available as a resource here.