Chat Application
From C4 Engine Wiki
The following is the complete source code of a complete C4 example application, which acts as a simple chat application. This application is used in the Basic Network Tutorial and Basic Network Tutorial - Part 2.
Usage: There's 4 commands used in total. Type 'server' to start a server. Type 'join <address>' to join a server at <address>, where address is an IP address. Type 'say xxx' to say xxx to everyone else on the server. Type 'name xxx' to change your name to xxx. See the tutorials for more information.
Contents |
Chat.h
#ifndef Chat_h #define Chat_h #include "C4Engine.h" #include "C4Application.h" #include "NameChangeRequestMessage.h" // Every application/game module needs to declare a function called ConstructApplication() // exactly as follows. (It must be declared extern "C", and it must include the tag "C4MODULEEXPORT".) // The engine looks for this function in the DLL and calls it to construct an instance of // the subclass of the Application class that the application/game module defines. extern "C" { C4MODULEEXPORT C4::Application *ConstructApplication(void); } namespace C4 { // These values define what protocol and what port to use - feel free to edit the game port if required. enum { kGameProtocol = 0x0000000B, kGamePort = 28327 }; class Chat : public Singleton<Chat>, public Application { private: // We first define two console commands. // The server command will start a new server Command serverCommand; // The join command will join an existing game. Command joinCommand; // The name change command will attempt to change a player's name. Command nameCommand; public: // Obligatory constructor / destructors. Chat(); ~Chat(); // This method will be executed whenever the user uses the server command. static void ServerCommand(const char *params); // This method will be executed whenever the user uses the join command. static void JoinCommand(const char *params); // This method will be executed whenever the user uses the name command. static void NameCommand(const char *params); // This method will be called by the engine whenever a chat is received. // It's used for a lot of other stuff, but that's outside the scope of this tutorial. void HandlePlayerEvent(PlayerEvent event, Player *player, const void *param); // This method is called whenever the local system receives a Message from a specific player. // On the server system, this method will respond to both locally sent messages (a system can send // messages to itself) or from one of the connected clients. For the client systems, this // method will almost always respond to messages from the server - a client cannot normally // connect to other clients directly. void ReceiveMessage(Player *from, const NetworkAddress& address, const Message *message); // This method will be called whenever the message manager receives a message. // Its task is to create an instance of a Message based on the MessageType given to it. Message *ConstructMessage(MessageType type, Decompressor& data) const; }; // This is a pointer to the one instance of the Chat class through which // any other part of the application/game module can access it. extern Chat * TheChat; } #endif
Chat.cpp
#include "Chat.h" using namespace C4; Chat *C4::TheChat = nullptr; C4::Application *ConstructApplication(void) { return (new Chat); } Chat::Chat() : Singleton<Chat>(TheChat), // Create the server command to bind to the text 'server' in the console. serverCommand("server", &ServerCommand), // Create the join command to bind to the text 'join' in the console. joinCommand("join", &JoinCommand), // Create the name command to bind to the text 'name' in the console. nameCommand("name", &NameCommand) { // Register the console commands with the engine. TheEngine->AddCommand(&serverCommand); TheEngine->AddCommand(&joinCommand); TheEngine->AddCommand(&nameCommand); // Set some settings in the network manager. TheNetworkMgr->SetProtocol(kGameProtocol); TheNetworkMgr->SetPortNumber(kGamePort); } Chat::~Chat() { } void Chat::ServerCommand(const char *params) { // Next, we start the server. The 'true' parameter // is to indicate that this machine is the server. TheMessageMgr->BeginMultiplayerGame(true); TheEngine->Report("Server initialized", kReportError); } void Chat::JoinCommand(const char *params) { // We'll first want to provide the user with some feedback - so he'll know what he's doing. String<128> str("Attempting to join "); str += params; TheEngine->Report(str, kReportError); // Next, we convert the entered parameters into a C4 NetworkAddress. // This format is used internally. It has both an IP address and a port number. NetworkAddress address = MessageMgr::StringToAddress(params); // We explicitly set a port in this example - it defaults to 0. address.SetPort(kGamePort); // Now we're just going to (try to) connect to the entered address. TheMessageMgr->Connect(address); } // Sends a message to the server requesting a name change. void Chat::NameCommand(const char *param) { NameChangeRequestMessage message(param); TheMessageMgr->SendMessage(kPlayerServer, message); } void Chat::HandlePlayerEvent(PlayerEvent event, Player *player, const void *param) { switch (event) { // We've received a chat. case kPlayerChatReceived: { // We'll want to display the player's name in front of the chat message, // so we'll first paste the player's name and his message together in a String object. // We limit the size of the displayed text using the String class, which automatically // cuts off text that exceeds the boundary set in the template parameter. String<kMaxChatMessageLength + kMaxPlayerNameLength + 2> text(player->GetPlayerName()); text += ": "; text += static_cast<const char *>(param); // Next, we'll make the completed message appear in the console. // The kReportError parameter tells the engine to put the message in the console. // It doesn't actually mean there's an error. TheEngine->Report(text, kReportError); break; } } // Finally, we pass the player event to the parent Application's HandlePlayerEvent method, // so it can display errors if needed. The method does nothing at the moment, but we'll // add it just in case it will somewhere in the future. Application::HandlePlayerEvent(event, player, param); } /* This method receives incoming messages as they come in, in the form of a data package. When it arrives, it's just a nondescript row of characters. This method will read the MessageType value and determine the type of the message, and return a new instance of the Message that supposedly handles the data contained in the Message. The engine will in turn call the returned object's Decompress method, which extracts the data from the package and assigns it to the fields in the message. Next, the ReceiveMessage is called with the object returned here, which will process the data contained in the Message. Both messages meant for the server and for the client will pass through here. Server-only messages (i.e. messages that are only received by the server) are marked as 'server', while client-only messages (i.e. messages that are only received by connected clients AND the server itself) are marked as 'client'. */ Message * Chat::ConstructMessage(MessageType type, Decompressor &data) const { switch(type) { // (Server) Name change request message case (kMessageNameChangeRequestMessage) : return (new NameChangeRequestMessage()); // (Client) Name change confirmation message. case (kMessageNameChangeMessage) : return (new NameChangeMessage()); // add cases for your other types of messages here. // the method returns nullptr when none of the message types match. default: return nullptr; } } /* The ReceiveMessage method receives and processes incoming Messages, checking their type and handling their contents accordingly. The different cases are marked as server and client similar to the above CreateMessage method - see that for the definition of server and client. */ void Chat::ReceiveMessage(Player *from, const NetworkAddress &address, const Message *message) { MessageType type = message->GetMessageType(); switch(type) { // (Server) Receive and process a name change request message. case (kMessageNameChangeRequestMessage) : { // First, we'll cast the generic Message into a more defined NameChangeRequestMessage, // so we can get the new name from it. const NameChangeRequestMessage * msg = static_cast<const NameChangeRequestMessage *>(message); // You can find the player who sent the message through the following code. Player * player = TheMessageMgr->GetPlayer(from->GetPlayerKey()); // Insert validation here, such as checking for duplicate player names, invalid player names, etc. // Next, we'll have to send a message to all connected clients informing them that a player's // name has changed. The message will be sent to the local client as well, // since we haven't updated the name yet. NameChangeMessage ncmsg(NameChangeMessage(msg->GetNewName(), player->GetPlayerKey())); TheMessageMgr->SendMessageAll(ncmsg); break; } // (Client) Receive a name change confirmation, change the name of the player contained in the message // locally. case (kMessageNameChangeMessage) : { // Cast the message const NameChangeMessage * msg = static_cast<const NameChangeMessage *> (message); // Find the player Player * player = TheMessageMgr->GetPlayer(msg->GetPlayerKey()); // change the name if the player exists. if (player) { // we write our confirmation message now, before we do the actual change. String<128> str("Player "); str += player->GetPlayerName(); str += " changed name to "; str += msg->GetNewName(); // Update the player's name player->SetPlayerName(msg->GetNewName()); // Confirm to the user TheEngine->Report(str, kReportError); } else { // we display a message that the player was not found. String<128> str("Player "); str += msg->GetPlayerKey(); str += " not found while trying to update name. =("; TheEngine->Report(str, kReportError); } break; } } }
NameChangeRequestMessage.h
#ifndef NameChangeRequestMessage_h #define NameChangeRequestMessage_h #include "C4Messages.h" namespace C4 { // This bit is important. In here, we give a unique identifier for our NameChangeRequestMessage, // so our program can determine whether the Message it receives is a NameChangeRequestMessage // or something else. // Each item in the list will automatically be assigned a numerical value, starting at 0. // Because there already are messages defined by the engine which already occupy the first // few numbers, we'll want to start counting where the engine stopped - hence the first line. // We'll have to move this definition to a central place when we try to add more messages. enum { kMessageServerInfo = kMessageBaseCount, // This is the identifier for our name change request message. kMessageNameChangeRequestMessage, // This is the identifier for our name change confirmation message kMessageNameChangeMessage }; /* The NameChangeRequestMessage is a message sent from a client to a server (one-way only) where the client requests a change in its player's name. The server can perform additional validation on the requested name, and when it agrees, it can send a NameChangeMessage back to the requesting client, as well as all the other connected clients, informing them that the player changed his name. */ class NameChangeRequestMessage : public Message { private: // We'll limit the length of a player's name with the global value set in // C4Messages.h. The String object will automatically cut off too long names in size. String<kMaxPlayerNameLength> newName; public: NameChangeRequestMessage(); // Constructor for child classes NameChangeRequestMessage(MessageType type); NameChangeRequestMessage(MessageType type, const char * name); // We'll only want to set a new player name through the constructor of the message. NameChangeRequestMessage(const char * name); ~NameChangeRequestMessage(); // We'll get the new name from the object through this Getter method. const char * GetNewName(void) const { return newName; } // These two methods will insert and extract the data // contained in a data package and put them into the class variables. void Compress(Compressor& data) const; bool Decompress(Decompressor& data); }; /* The NameChangeMessage is sent from the server to all currently connecting clients, informing them that the player matching the given PlayerKey has changed his name. When this message is sent, the server already validated and accepted the name change, so receiving clients shouldn't need to do any additional validation. */ class NameChangeMessage : public NameChangeRequestMessage { private: // We'll only store the player's Key, an identifier which is unique // for each player playing on a server. PlayerKey playerKey; public: // The constructors are the only way to set the new name and the new player key. // The constructors in the NameChangeMessage message type call the parent constructor // (that of NameChangeRequestMessage) with the MessageType of the NameChangeMessage (see above). NameChangeMessage(); NameChangeMessage(const char * name, PlayerKey pk); ~NameChangeMessage(); // The compress and decompress methods are obligatory. void Compress(Compressor& data) const; bool Decompress(Decompressor& data); // Simple accessor for our player key value const PlayerKey GetPlayerKey() const { return playerKey; } }; } #endif
NameChangeRequestMessage.cpp
#include "NameChangeRequestMessage.h" using namespace C4; // The primary constructor tells the parent class (Message) what identifier this Message uses. // Used in the CreateMessage method. NameChangeRequestMessage::NameChangeRequestMessage() : Message(kMessageNameChangeRequestMessage) { } // The secondary constructor is used by subclasses, and passes the MessageType parameter upwards // to the Message constructor. NameChangeRequestMessage::NameChangeRequestMessage(MessageType type) : Message(type) { } // This constructor does the same as the above, with the addition of also setting the newName // field with the name parameter. NameChangeRequestMessage::NameChangeRequestMessage(MessageType type, const char *name) : Message(type) { newName = name; } // This constructor creates a new NameChangeRequestMessage with the default NameChangeRequestMessage MessageType, // also setting the new name field to the name indicated in the parameter. NameChangeRequestMessage::NameChangeRequestMessage(const char *name) : Message(kMessageNameChangeRequestMessage) { newName = name; } // This destructor does nothing at the moment. It's there for asthetical purposes. NameChangeRequestMessage::~NameChangeRequestMessage() { } // Our Compress method has to add our newName value to the Compressor data variable // passed by the engine to our name change request message. void NameChangeRequestMessage::Compress(Compressor &data) const { data << newName; } /* Our Decompress method extracts the information from the Data variable and places it in our newName variable. Officially, we also add validation logic into this method to determine the validity of the contents of this message. This isn't really needed in this case though, so we just return true and pretend we did validation. */ bool NameChangeRequestMessage::Decompress(Decompressor &data) { data >> newName; return (true); } // Below follows the NameChangeMessage class implementation. // Empty constructor, used by the ConstructMessage() method. Just calls the parent's constructor. NameChangeMessage::NameChangeMessage() : NameChangeRequestMessage(kMessageNameChangeMessage) { } // This constructor also just calls the parent constructor with a new messageType, passing the name parameter to the superclass as well. NameChangeMessage::NameChangeMessage(const char *name, PlayerKey pk) : NameChangeRequestMessage(kMessageNameChangeMessage, name) { playerKey = pk; } // The destrutor does nothing (besides destructing). NameChangeMessage::~NameChangeMessage() { } // We'll add the NameChangeMessage's data (i.e. the playerKey) to the data package, then // call the parent's compress method so it can add its data to the package. void NameChangeMessage::Compress(Compressor &data) const { data << playerKey; NameChangeRequestMessage::Compress(data); } // We'll do the same with the decompress method, we first get our data from it, // then pass it to the parent. Here we also pretend we've validated the data, // by just returning true. bool NameChangeMessage::Decompress(Decompressor &data) { data >> playerKey; NameChangeRequestMessage::Decompress(data); return(true); }
