CS/IMGD 411x Project 2

Yak - Multiperson Internet Messaging

[Yak]

Due date: Sunday, September 15, 11:59pm
Due date: Tuesday, September 17, 11:59pm


Overview

There are three main goals to this project:

  1. Socket programming: to write C++ code that uses Internet sockets for client-server communication, useful not only for this project but for socket-based programming of any kind.

  2. Network support in game engine: to add general-purpose network support to a game engine as both a learning experience and as a foundation for projects 3 and 4.

  3. Multiperson Internet messaging: to use the above - socket programming and game engine networking - to make a multiperson, Internet messaging application. Doing so demonstrates mastery of socket programming and networking support, and continues progression of game development skills.


Overview | Details | Tips | Submission | Grading


Details

You are to make an Internet messaging application - Yak - which supports two or more users, each on their own computers, exchanging text messages over the Internet in realtime. Features of the application include:

To realize these features, you will add use TCP sockets and add networking support to the Dragonfly game engine and then implement a client-server application that uses them.


Details: Sockets | Networking | Yak


Socket Programming

The following system calls will be used in your socket implementation:

  // OS support for sockets.

  // Socket functions.
  socket()
  getaddrinfo()
  connect()
  accept()
  listen()
  bind()
  send()
  recv()

  // Flags.
  MSG_PEEK

  // Data types.
  struct sockaddr_in 

For namespace reasons, the syntax :: may be needed in front of system calls (e.g., ::send(...)).

Unlike many system calls, socket code works nearly the same on Windows, Mac and Linux. However, there are a few differences in some of the function names and parameters, as well as using libraries and needed include files. The following code is Linux/Mac specific:

  // Linux/Mac specific support for sockets.

  // Header files.
  #include <fcntl.h>
  #include <netdb.h>
  #include <netinet/in.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <unistd.h>
  #include <sys/ioctl.h>
  #include <sys/socket.h>

  // System calls.
  close()
  ioctl() 
  sigaction()

  // Flags.
  MSG_DONTWAIT
  O_NONBLOCK
  
  // Data types.
  socklen_t

The following code is Windows-specific:

  // Windows-specific support for sockets.

  // Header files.
  #include <WinSock2.h>
  #include <WS2tcpip.h>
  #include <io.h>

  // System calls.
  closesocket()
  ioctlsocket()
  WSAStartup()
  WSACleanup()

  // Data types.
  WSADATA

In order to use sockets in Windows, the windows socket library needs to be linked in during compilation and started up (once only) during runtime. Below are some code fragments that show key operations:

  // Windows: link in and use socket library.
  #pragma comment(lib, "ws2_32.lib")
  WSADATA wsa;
  WSAStartup(MAKEWORD(2, 2), &wsa);
  WSACleanup();

Sample code is provided below that shows complete, working process-to-process communication over TCP sockets:

Note, while the above examples show one-way communication, sockets are two-way - i.e., you can send and receive from the same connected socket.

On all systems, sockets are by default blocking, meaning if a process reads from them and there is no data available, the process blocks until there is data. On Linux and Mac, recv() can be made non-blocking with the MSG_DONTWAIT flag, and the socket itself made non-blocking with an fcntl() call with O_NONBLOCK.

  // Linux/Mac: make (int) socket non-blocking.
  int flags = O_NONBLOCK;
  fcntl(sock, F_SETFL, flags;

On Windows, a socket can be made non-blocking with an ioctlsocket() call with FIONBIO:

  // Windows: make (int) socket non-blocking.
  u_long mode = 1;
  ioctlsocket(socket, FIONBIO, &mode);

On Linux and Mac, the ioctl() call can be used to retrieve the number of bytes available in a socket before reading (e.g., to make sure there is a complete message) using the FIONREAD parameter as the second argument:

  // Linux/Mac: check bytes on (int) socket.
  int bytes;
  ioctl(socket, FIONREAD, &bytes);

On Windows, the equivalent call is with ioctlsocket() and FIONBIO:

  // Windows: check bytes on (int) socket.
  u_long bytes;
  ioctlsocket(socket, FIONREAD, &bytes);

Details: Sockets | Networking | Yak


Dragonfly Support for Networking

Recommended are three classes to provide networking support in Dragonfly: 1) a network manager, 2) a network event, and 3) a sentry to support network interaction between relevant classes. While these classes will not technically be part of the Dragonfly engine (the engine is an immutable library, unless you have the source from implementing the engine from scratch), the new network classes can easily be accessed through an additional library (e.g., libnetwork.a) you make and, hence, will behave as if they are part of the engine.

Since the added classes are intended to function as part of the engine, they could be placed in Dragonfly's df:: namespace. This requires code outside of the namespace (e.g., game code) to use df:: to access elements inside the namespace (similar to access to all Dragonfly services). Typically large, 3rd-party libraries (such as a game engine) use namespaces to help developers avoid conflicts in names their own code may use with names the libraries use. The Dragonfly namespace is meant to prevent potential name conflicts with game code.

NetworkManager

The NetworkManager handles the internals of a game's network connection. As such, both a game client and a game server have a NetworkManager. Like other Dragonfly managers, the NetworkManager class inherits from the Manager base class and is a singleton. It is typically started up when the game engine starts and shut down when the game engine stops. Its main private attributes handle sockets -- data types that are int variables. The suggested class header file is provided below:

  // 
  // NetworkManager.h
  //

  #ifndef NETWORK_MANAGER_H
  #define NETWORK_MANAGER_H

  // System includes.
  #include <string>
  #include <vector>

  // Engine includes.
  #include "Manager.h"

  // Two-letter acronym for easier access to manager.
  #define NM df::NetworkManager::getInstance()

  namespace df {

  const std::string DRAGONFLY_PORT = "9876";

  class NetworkManager : public Manager {

   private:
    NetworkManager();                       // Private since a singleton.
    NetworkManager(NetworkManager const&);  // Don't allow copy.
    void operator=(NetworkManager const&);  // Don't allow assignment.
    std::vector<int> m_sock;                // Connected network sockets.
    int m_accept_sock;                      // Socket for accept connections.

   public:

    // Get the one and only instance of the NetworkManager.
    static NetworkManager &getInstance();

    // Start up NetworkManager.
    int startUp() override;

    // Shut down NetworkManager.
    void shutDown() override;

    // Setup NetworkManager as server (if false, reset to client).
    // Return 0 if ok, else -1.
    int setServer(bool server = true, std::string port = DRAGONFLY_PORT);

    // Return true if successfully setup as server, else false.
    bool isServer() const;

    // Accept network connection.
    // If successful, generate EventNetwork (accept).
    // Return sock index if new connection (note, 0 for first).
    // Return -1 if no new connection, but no error.
    // Return -2 if error.
    int accept();

    // Make network connection to host at port.
    // If successful, generate EventNetwork (connect).
    // Return socket index if success, -1 if error.
    int connect(std::string host, std::string port = DRAGONFLY_PORT);

    // Get number of connected sockets.
    int getNumConnections() const;

    // Send bytes from buffer to connected network socket index.
    // Return 0 if success, -1 if error.
    int send(const void *buffer, int bytes, int sock_index=0);

    // Receive from connected network socket index (no more than nbytes).
    // If leave is true, don't remove data from socket (peek).
    // Return number of bytes received, -1 if error.
    int receive(void *buffer, int nbytes, bool leave, int sock_index=0);

    // Close network connection on indicated socket index.
    // If successful, generate EventNetwork (close).
    // Return 0 if success, else -1.
    int close(int sock_index=0);

    // Close all network connections.
    // If successful, generate EventNetwork (close), for each.
    // Return 0 if success, else negative number.
    int closeAll();

    // Return true if socket index is connected, else false.
    bool isConnected(int sock_index=0) const;

    // Check if network data on indicated socket index.
    // Return amount of data (0 if no data), -1 if not connected or error.
    int isData(int sock_index=0) const;

    // Return system socket from socket index, -1 if error.
    int getSocket(int sock_index=0) const;
  };

  } // end of namespace df

  #endif // NETWORK_MANAGER_H

The major NetworkManager methods are described next.

Upon startup(), the NetworkManager initializes m_accept_sock to -1 (indicating the NetworkManager is not yet connected and not configured as a server) and the array of sockets (m_sock) is cleared. Upon shutdown(), if m_accept_sock is not -1, it is closed via the system call. Similarly, each socket in the m_sock vector is closed and the m_sock array cleared.

The setServer(true) method puts the NetworkManager in server mode, able to accept socket connections from clients. This means creating the socket to use for incoming connections (socket()), binding to it (bind()), and listening (listen()). Typical of most Internet servers, the NetworkManager listens on a "well-known" port (for most network games, this is defined by the game developer) that the client uses when connecting. The default here can be specified via DRAGONFLY_PORT. The assumption is also that an external naming service provides the IP address and/or hostname where the server (game host) is running. It also makes the socket non-blocking (using ioctlsocket() or fcntl()) so later checks on the socket for connections via accept() don't block if there are no new connections. Conversely, setServer(false) sets the manager up as a client (the default). If it was previously setup as a server, the socket created for accepting connections is closed.

The accept() method checks for new connectiopns from clients being somewhere else on the Internet (e.g., having a different Internet address). Inside accept(), the system ::accept() call is invoked and, if there is a new connection, the socket added to the m_sock vector.

The complimentary method, connect(), is invoked by the client to connect to the server. As such, it takes as parameters the server's hostname and port as strings. When invoked, connect() does the following steps: 1) lookup the host name, translating it into the necessary system structure for connections (getaddrinfo()), 2) create a socket used by the client to connect to the server (socket()), and finally 3) connect to the server (connect()).

The method isConnected() can be used by either the client or the server to indicate if the NetworkManager has a connected socket. It returns true if there is a socket in the socket vector (m_sock), and otherwise returns false. The method getSocket() returns the actual socket used for the OS system calls (remember, outside of this, game code that uses the network manager has the socket index only).

The method send() sends data through the connected socket (using the ::send() system call).

The method isData() returns the amount of data (in bytes) that is waiting in the network socket (using ioctlsocket() or ioctl()), leaving any data there intact. If there is no data, zero is returned, but this is not considered an error.

The method receive() retrieves data from the connected socket (using the ::recv() system call). Receive is non-blocking, so if there is no data pending, the method does not block but returns (the ::recv() call returns an error, and reports EAGAIN or EWOULDBLOCK as "error" codes in this case). If leave is true, then the data is left on the socket after the system call (by using the MSG_PEEK flag as a parameter).

EventNetwork

Like all game engines, Dragonfly uses events to pass messages between the engine and game objects. For networking support for Dragonfly, you will create a new event - the EventNetwork. The NetworkManager generates appropriate EventNetwork events for three cases: when a connection is accepted (in accept()), when a connection is connected (in connect()) and when a connection is closed (in close()). When there is network data, that is generated by the Sentry. The EventNetwork class is described below.

  //
  // EventNetwork.h - A network event.
  //

  #ifndef EVENT_NETWORK_H
  #define EVENT_NETWORK_H

  // System includes.
  #include <string>

  // Engine includes.
  #include "Event.h"

  namespace df {

  const std::string NETWORK_EVENT = "df-network";

  enum class NetworkEventLabel {
    UNDEFINED = -1,
    ACCEPT,
    CONNECT,
    CLOSE,
    DATA,
  };  

  class EventNetwork : public Event {

   private:
    EventNetwork();            // Must provide label in constructor.
    NetworkEventLabel m_label; // Label of event.
    int m_socket_index;        // Socket index of connection.
    int m_bytes;               // Number of bytes in message (0 if connect).

   public:
    // Constructor must provide label.  
    EventNetwork(NetworkEventLabel label);

    // Set label.
    void setLabel(NetworkEventLabel new_label);

    // Get label.
    NetworkEventLabel getLabel() const;

    // Set socket index.
    void setSocketIndex(int new_socket_index);

    // Get socket index.
    int getSocketIndex() const;

    // Set number of bytes in message.
    void setBytes(int new_bytes);

    // Get number of bytes in message.
    int getBytes() const;
  };

  } // end of namespace df

  #endif // EVENT_NETWORK_H

The network event labels (ACCEPT, CONNECT, CLOSE, and DATA) are specified in their own enum class. Implementation of the EventNetwork class is fairly straightforward as the class is mostly a container. The proposed design does not include embedding the network data in the event object (although it could be changed). Instead, the network data is left in the socket. This assumes that any interested game object will pull the data from the socket via NetworkManager (for your Yak application, that is the Client object and the Server object - see below).

The constructors should set the event type (via Event::setType() in the base Event class) to df::NETWORK_EVENT and all other attributes to default values (bytes to 0 and socket_index to -1).

The other methods are to get/set the label (df::NetworkEventLabel), socket index and number of bytes, respectively.

Sentry

If the NetworkManager was actually implemented in Dragonfly, the engine would provided notification of all network events - i.e., when network data arrived, the engine would generate an EventNetwork for all game objects that had registered interest. However, for this project, since the NetworkManager is implemented in game code, there needs to be a game object running on the client(s) and server that generates network events. This can be most easily done by a "sentry" game object (called Sentry) that is derived from Object and registers for step events. Every step, the Sentry polls the network manager and, when network data has arrived, generates network events. Since the Server/Client registers for interest in network events (see below), they are then notified that network data has arrived and can handle it appropriately.

  //
  // Sentry.h - Poll NetworkManager for incoming messages.
  //

  // Engine includes.
  #include "Object.h"

  namespace df {

  class Sentry : public Object {

   private:
    void handleStep();

   public:
    Sentry();
    int eventHandler(const Event *p_e) override;
  };

  } // end of namespace df

The Sentry eventHandler() method checks if the event is a step event and, if so, calls handleStep() which does the real work. The handleStep() method has a few checks: 1) If the NetworkManager is configured as a server (isServer() is true), the Sentry calls NM.accept() to accept any new network connections. Note that the NetworkManager generates an appropriate EventNetwork when a connection is established. 2) If the NetworkManager is not yet connected, there is nothing else to do - i.e., there are no connected sockets to check for data.

But if there are one or more connected sockets, the Sentry loops through all connected sockets (using NM.getNumConnections()) and for each connected socket: 1) See if there are enough bytes for an int. If so, pull out the first 4 bytes (sizeof(int)) as an int but leave the data in the buffer (i.e., leave is true in the NM.receive() call). This integer's value holds the number of bytes in the full message (all headers included). If there are enough bytes in the socket for the full message (i.e., the bytes available in the socket is greater than or equal to this int), generate a network event (with label DATA) and send to all interested objects by calling WM.onEvent().


Details: Sockets | Networking | Yak


Yak

With the socket code in place and the foundations for network support in Dragonfly, you can build the Yak multiperson Internet messaging application. You will actually make two programs - a Yak client and a Yak server. The server will be headless (no display), while the client will have a GUI with a text box for entering chat and another, larger text box one for displaying chat messages from all users.

User Interface

The client needs to provide a user interface for users to connect to the server, enter their handle/name, and then enter chat messages to other users. In Dragonfly, this is most easily done with a TextEntry object, a special type of game object that gets keyboard input.

Once the user is chatting, the interface needs to display the chat messages on the screen. In Dragonfly, this is most easily done with a TextBox object, a special type of game object that displays text, word-wrapping long lines and scrolling the text off the top of the box when there are more lines than the box can handle.

Sample code showing use of these classes (TextEntry and TextBox) in an interface suitable for chatting is provided below:

Yak Server

The server accepts connections from Yak clients. Once connected, it then receives messages on any connected socket. Messages that contain data (e.g., a typed message from a user, such as "bob: Hello!") are sent to all clients.

Below is a screenshot showing 3 chatting clients connected to a server which is headless, so not shown. (A server can be made headless - i.e., it doesn't need a graphical display - by setting headless:true, in the df-config.txt Dragonfly configuration file.)

[Yak Screenshot]

Like all client-server applications, Yak needs to specify the protocol for clients and the server to communicate. Generally, communication will use a flexible message structure that specifies data in a specific format: 1) the size of the message (in total bytes) is first, 2) followed by the type of the message, and 3) lastly, the message data (if needed).

Message types are preferably specified via an enum class, such as:

  // Types of messages between Yak client and server.
  enum class MessageType {
    UNDEFINED = -1,
    EXIT,
    CHAT,
  };

For example, a client may send a "hello" message typed by the user to the server. The text itself is 5 bytes. The type of the message is CHAT (indicating this is a chat message the server should send to all clients). The size of the message type is an enum class is 4 bytes (an int). The message will also need to contain another int (another 4 bytes) that has the total message size. So, the total message size is 5 bytes ("hello") + 4 bytes (CHAT) + 4 bytes (the total message size) or 13 bytes. The client would format the message as [13, CHAT, "hello"] (in binary form, no commas or quotes) and send it to the server.

The suggested design is for the Yak server to have a Server object to handle network aspects (e.g., setting up the Network Manager as a server, handling network events, etc.).

The server flow would then be:

Yak Client

The Yak client: 1) first connects to an Internet host, indicated by the user, 2) gets the user's name/handle, 3) gets input from the user, packages it into a message and sends it to the server, and 4) receives data from the server, and 5) extracts it and displays it in the chat window.

The suggested design is for the Yak client is to have a Client object to handle network aspects (e.g., setting up the Network Manager as a server, handling network events, etc.).

The client flow would then be:


Overview | Details | Tips | Submission | Grading


Tips

This section has tips that may be helpful as you design and programming this project.

Socket Errors

All system calls should be checked for failure, and this is true for the socket system calls as well. While the return codes indicate success or failure, details how why the call failed can be helpful to the programmer (and, in some cases, the user of the application). For socket code, how the error details are reported depends upon the operating system. Below are examples for error codes reported by Windows versus Linux/Mac.

  // Windows - reporting socket error codes.
  LPTSTR error_text = NULL;
  FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM
    | FORMAT_MESSAGE_ALLOCATE_BUFFER
    | FORMAT_MESSAGE_IGNORE_INSERTS,
    NULL, 
    WSAGetLastError(),
    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
    (LPTSTR)& error_text,  /* Output. */
    0, /* Min. size for output buffer. */
    NULL);
  if (error_text != NULL) {
    fprintf(stderr,"Error! number: %d  message: %s\n",
                   WSAGetLastError(), error_text);
    LocalFree(error_text);
    error_text = NULL;
  }
  // Linux/Mac - reporting socket error codes.
  fprintf(stderr,"Error! number: %d  message: %s\n",
                  errno, strerror(errno));

Dragonfly Configuration

The Dragonfly engine writes messages (e.g., errors, information) to a logfile. This can be helpful for debugging and can be used in your game code (e.g., reporting errors in the socket code above). By default, the logfile's name is dragonfly.log but this can be changed via environment variables (dynamic values that are used to pass configuration settings to process) and/or in the dragonfly configuration file (default df-config.txt). The environment variables for game-specific settings:

This can be useful to, say, have specific settings for the server (e.g., the server can be "headless" without a display, or for the client to have the logfile name use the process id). These variables can be set for your shell (e.g., Bash or Powershell), but can also be set in the process itself via a system call. Setting environment variables in code in Windows is done via _putenv_s() and in Linux/Mac via setenv(). For example:

  // Windows.
  _putenv_s("DRAGONFLY_CONFIG", "df-config-server.txt");

  // Linux/Mac.
  setenv("DRAGONFLY_CONFIG", "df-config-server.txt", 1);

Misc

The Dragonfly Button class may also be of interest in building the Yak client interface. A Button is a special type of game object that displays text in a small, bordered box, can respond to mouse hovering by changing color and responds to a callback when clicked. Below is a basic example.

  // CustomButton.h - illustrate basic Dragonfly button.

  // Engine includes.
  #include "Button.h"
  #include "Color.h"
  #include "LogManager.h"

  class CustomButton : public df::Button {
   public:
    // Called when Button clicked.
    void callback() override;
  };

  // Default constructor sets values.
  CustomButton::CustomButton() {
    setViewString("Button");
    setBorder(true);
    setLocation(df::CENTER_CENTER);
    setColor(df::CYAN);
    setHighlightColor(df::YELLOW);
  }

  // On callback, write message to logfile.
  void CustomButton::callback() {
    LM.writeLog("CustomButton(): clicked!");
  }

To make sending messages easier for your Yak server and client, you may want to make a utility function:

  // Pack and then send the message via the NetworkManager.
  int sendMessage(const char *msg, int msg_size, MessageType type, int sock_index);

The function would compute the total message size (including all headers), then write the size, message type and message data to a buffer. Then, the NetworkManager would be used to send the buffer to the indicated socket index. You could make a corresponding recvMessage() utility function, too.

Compilation errors such as "Redeclaration of class ClassName..." (with the actual class name instead of ClassName) typically occur if the header file, say "ClassName.h", has been included from multiple source files. This error occurs most often for utility-type classes (and functions) that are used by multiple other classes (e.g., the NetworkManager and EventNetwork). The typical fix is to use conditional compilation directives for the compiler pre-processor (e.g., #ifndef and #define). See the NetworkManager.h and EventNetwork.h examples above, for reference. Note, if conditional compilation directives are in use, they should be checked that the names used in the #ifndef and #define statements are identical.

For more detailed socket-specific programming help, you might try Beej's Guide to Network Programming - Using Internet Sockets.


Overview | Details | Tips | Submission | Grading


Submission

Your assignment is to be submitted electronically (via Canvas) on the time and day due. You must hand in the following:

  1. Source code:

  2. A README file listing: platform (Windows, Mac or Linux), how to compile (if there are special settings), how to run (ditto) and anything else needed to understand (and grade) your application.

  3. VIDEO showing: A) your code compiling, B) your Yak server starting up, C) 2+ chat clients starting up and connecting to your server, D) users on 2+ screens typing and having that data echoed on all screens, and E) clients and server gracefully shutting down. The video should be 10 minutes or less. It does not need to be polished, just complete. Recording via Zoom can work well. The video can be included in the submission or hosted online and with a link to follow.

Before submitting, "clean" your project:

This will remove all the temporary (compiled) files. Failure to do may mean you file is too big to submit!

You do not need to submit the Dragonfly library, header files, nor the SFML files.

Use zip to archive your files.

To submit your assignment (say, lastname-proj2.zip) via Canvas:

Open: Assignment - Project 2
Click: Upload
Click: Drag your zip file,
Select: lastname-proj2.zip
Click: Submit Assignment

When successfully submitted, you should see a message similar to:

SUBMITTED on September 09, 2024 3:53pm

Important - you must click the Submit Assignment button at the end or your file will not be submitted!


Overview | Details | Tips | Submission | Grading


Grading Guidelines

Breakdown

Sockets - 30% : Correctly function socket code (demonstrated and testing) is worth about 1/3 of the grade. This includes a single server with 2+ clients that can connect and exchange messages. Understanding and implementing this code is crucial to get networking support in Dragonfly.

Networking Support - 30% : Adding networking support to Dragonfly is worth about 1/3 of the grade. Understanding this code, and getting it implemented and debugged is crucial for building multiplayer network games moving forward.

Yak - 30% : Designing and implementing the Yak application using the networking support code (and sockets) is worth the remaining third of the grade. Doing so demonstrates your knowledge of the networking support code and further demonstrates your game programming ability.

Documentation - 10% : Not to be overlooked is including the documentation provided, as well as having that documentation be clear, readable and pertinent to the assignment. This primarily includes the README described above but also any needed supporting information. Having well-structured, readable and commented code is part of Documentation, too. Getting in the habit of good documentation is important for large software projects, especially when done in teams.

Note, bugs and/or code that has not been tested can result in points taken off for any of the areas above.

Rubric

100-90. The submission clearly exceeds requirements. The networking code is in place, handles error conditions robustly and is clear and well-structured. The support for networking in the Dragonfly engine is clearly designed, implemented and tested. The Yak client and server work flawlessly and there is demonstrated functionality for more than 2 clients and an authoritative server. Documentation is thorough and clear and code is well-structured and readable.

89-80. The submission clearly meets requirements. The networking code is in place, handles all error conditions and is well-structured. The support for networking in the Dragonfly engine is functionally implemented and tested. The Yak client and server work well and there is demonstrated functionality for more than 2 clients and an authoritative server. Documentation and code is clear.

79-70. The submission barely meets requirements. The networking code is in place, but does not handle all error conditions, and may be hard to follow. The support for networking in the Dragonfly engine is functional, but not well-implemented and/or tested. The Yak client and server work, but not all functionality is demonstrated or tested. Documentation is present, but may be unclear or missing aspects and code is readable but may be unclear in parts.

69-60. The project fails to meet requirements in key places. The networking code is missing aspects and does not handle many error conditions, and may be hard to follow. The support for networking in the Dragonfly engine is lacking and/or not well implemented and unclear, and has not be tested/is not robust. The Yak client and server do not work completely (e.g., may crash in some cases). Documentation is lacking and code is unclear and/or unreadable in large parts..

59-0. The project does not meet many of the key requirements. The networking code is missing and/or does not work. The support for networking in the Dragonfly engine is missing significant parts and/or not tested. The Yak client and server do not work completely (i.e., always crash). Documentation is woefully inadequate or missing and code is difficult to read.


Overview | Details | Tips | Submission | Grading


Return to the IMGD 411x home page