A DirectPlay Tutorial
V 1.2 11-21-2001
by Sobeit Void

Updates

V1.2 (21-11-2001)
- Fixed typo in Create_TCP_Connection code snippet
- CoUnIntialize() should be CoUninitialize()
- IID_LPDIRECTPLAYLOBBY2A should be IID_IDirectPlayLobby2A

V1.1 (11-4-2000)

Abstract

Seeing that there are few (if any) tutorials on DirectPlay, and that it is such a pain to learn DirectPlay from the MSDN (like I have), I thought I would alleviate some of the agony by writing this tutorial. Also I have this day off since I'm waiting for the monstrous 100 MB DX 7 SDK download.

If you find any errors, please email me at robin@cyberversion.com

What This Tutorial is About

The tutorial will use Directx 5.0 or the IDirectPlay3 interface. I know DX 7.0 is out but I am still downloading it and I passed with DC 6. Although DC 5.0 is inefficient (every packet has high overhead), it seems the new versions are getting better. Anyway, the idea should mainly be the same.

The demo will be a chat program that I am using for my game. It is not a lobby server (a lobby server is something like Blizzard's BattleNet), just a client/server program. The user is assumed to be familiar with C/C++ and some Win32 programming (Of course, DX too). Some networking concepts would help. No MFC, I hate MFC. Putting another layer on Win32 API bothers me when you can access Win32 functions directly. Also, I will only use TCP/IP. If you want to use IPX, modem, or cable, check out the MSDN. They are roughly equivalent.

This tutorial by no means teaches you everything about DirectPlay. Any networking application is a pain to write, and you should consult the MDSN documentation for more information (mostly where I learnt DirectPlay from).

There is no sample demo exe too as I don't have time to write one out to show you.

Let's get started

What DirecPlay is About

DirectPlay is a layer above your normal network protocols (IPX, TCP/IP etc). Once the connection is made, you can send messages without needing to know what the user is connecting with. This is one of the best features (IMO) and though some of you may come up with more efficient messaging protocols with WinSock, I'd rather use DirectPlay and save me hordes of trouble. BTW, DirectPlay uses Winsock too.

Sessions in DirectPlay

A DirectPlay session is a communication channel between several computers. An application must be in a session before it can communicate with other machines. An application can either join an existing session or create a new session and wait for people to join it. Each session has one and only one host, which is the application that creates it. Only the host can change the session properties (we will get to that detail later).

The default mode in a session is peer-to-peer, meaning that the session complete state is replicated on all the machines. When one computer changes something, other computers are notified.

The other mode is client/server, which means everything is routed through a server. You manage the data in this mode, which is probably the best in a chat program. However, I used sort of a hybrid in this tutorial.

Players in DirectPlay

An application must create a player to send and receive message. Messages are always directed to a player and not the computer. At this point, your application should not even know about the computer's location. Every message you sent is directed at a specific player and every received message is directed at a specific local player (except for system messages; more on that later). Players are identified only as local (exist on your computer) or remote (exist on another computer). Though you can create more than one local player on your machine, I find it not very useful. DirectPlay offers additional methods to store application specific data so you don't have to implement a list of players but I'd rather do my own list.

You can also group players in the same session together so any message sent to you will get directed to the group. This is great for games where you can ally with other people and you want to send messages to your allies only. Unfortunately, you have to explore that area on your own.

If at this point you find all this very troublesome, try writing your own network API. You will be glad what DirectPlay does for you.

Messages in DirectPlay

Now we have a session and a player, we can start sending messages. As I said, each message sent is marked by a specific local player and can be sent to any player. Each message received is placed in a queue for that local player. Since I create only one player, all the messages belong to that player. You don't need to bother about this queue; all you need is to extract the messages from it and act on them. The application can poll the receive queue for messages or use a separate thread and events. I tried the thread method and it works great, except for when I use MessageBox to notify the user (in case you don't know, MessageBox displays a message box in Windows). If you want to use threads, you need to pause the thread when the application displays a message box and somehow it gets very messy with synchronization. So I opted for the poll method. Feel free to use threads if you think you can handle it.

There are two types of messages: player and system. Player messages have a sender and receiver. System messages are sent to every player and are marked sent from the system (DPID_SYSMSG). DP stands for DirectPlay, ID for identification. If you cannot understand messages, go and learn more about DirectDraw first. System messages are generated when the session state is changed, i.e. when a new player joins the session.

Note: There are also security features using the Security Support Provider Interface (SSPI) on windows. These messages are encrypted and such. I don't think this is of much use in gaming.

Actual implementation

Whew. Now we get to more details. If you didn't quite understand any of the above, please read them again till you do. If you have any questions like "What if..." it will be answered soon. So let's move on.

The first thing to do is to include the DirectPlay header files:

#include <dplay.h>     // directplay main
#include <dplobby.h>   // the directplay lobby 

Also add DPLAYX.LIB to your project.

If you are wondering why there is a dplay.lib and dplayx.lib, add the dplayx.lib cause I think there are more methods there used in the lobby. Also if you are asking why am I including the dplobby methods when I am not using a lobby server, it will become clearer later.

Also you need to define INITGUID or add the dxguid.lib. Define this at the very top of your project.

#define   INITGUID      // to use the predefined ids

Next you need to give you application a GUID (Global Unique Id). This ID is to distinguish the application in the computer. You don't want your application to send messages to your browser, only your application. You can use guidgen.exe to create a id for your application. Microsoft guarantees that it will never mathematically create the same GUID twice, so we take their word for it. It will look something like

DEFINE_GUID(our_program_id,
0x5bfdb060, 0x6a4, 0x11d0, 0x9c, 0x4f, 0x0, 0xa0, 0xc9, 0x5, 0x42, 0x5e);

Now to define our globals

LPDIRECTPLAY3A       lpdp = NULL;        // interface pointer to directplay
LPDIRECTPLAYLOBBY2A  lpdplobby = NULL;   // lobby interface pointer

If you are wondering what the A behind the interface stands for, it means ANSI version. There are two versions for DirectPlay – ANSI and Unicode. (Unicode is a standard for using 16 bits to represent a character instead of 8 bits, just for internationalization. Just use the ANSI version and forget about supporting multiple languages. Makes everybody happy.)

The next thing is the main loop of the program. This is the bare skeleton of what it looks like.

// Get information from local player
// Initialize Directplay connection from the information from above

while(1)
{
    if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)
    {
        // Your normal translating message

    } // end if PeekMessage(..)
    else
    {
        // Your game loop

        // Receive  messages – We will implement this
    } // end else
} // end of while(1)

// Close Directplay Connection

How you get information from the user is up to you. You can do it via a dialog box or any other way you deem. The main thing you need to get is the name and whether if it is the server or client. If it is the server, you get the session name. If it is the client, you get the TCP/IP address to connect to. Anyway, we store those in a global below:

BOOL   gameServer;         // flag for client/server
char   player_name[10];    // local player name, limit it to 10 chars
char   session_name[10];   // name of session, also limit to 10 chars
char   tcp_address[15];    // tcp ip address to connect to

I'm sorry if you are firmly against globals. Feel free encapsulate them, but I think globals simplify the learning process here. Also I do not do much error checking here; theoretically you should test the result of every function call.

Now before we move on, we should implement a list of players in the current session. Although it will not be necessary here, you will need it in larger applications.

You create a list with the following item element:

Class DP_PLAYER_LIST_ELEM{

DPID    dpid;      // the directplay id of player
char    name[10];  // name of player
DWORD   flags;     // directplay player flags

// any other info you might need
};

You need to implement a list class that adds a player and deletes a player with a specific dpid. Do not reference the players by their names because players can have the same name. Use the id to differentiate players. Due to space constraints, I will not include any code here. Alternatively you can use arrays to hold the global player information, but this is not scalable and more troublesome.

Then we define a global pointer to the class:

DP_PLAYER_LIST   *dp_player_list;   // list of players in current session

That is about all the globals you need. You should also create a local player struct for additional information for local players only.

Setting up Connection

Initialization

To set up the connection, we will write a function that takes a TCP/IP string and create a TCP/IP connection. This is also where the lobby interface comes in.

Side note: Although DirectPlay has a method for enumerating the connections available, the method will enumerate all connections even if they are not available. So if you do not have an IPX connection, the enumeration will return an IPX option to connect and the connection would fail only when the user tries to make the connection. This seems redundant to me so I recommend you skip this part and give the options to the user straight, failing only when the user tries to make the connection.

In case you don't know about enumeration, we will talk more about it later. Enumerating things in DirectX is for the user to provide a function that is called (repeatedly) by a process in DirectX. This is known as a callback function.

Here goes the function. Remember you should do error checking for every function call.

int Create_TCP_Connection(char *IP_address)
{
    LPDIRECTPLAYLOBBYA        old_lpdplobbyA = NULL;    // old lobby pointer
    DPCOMPOUNDADDRESSELEMENT  Address[2];               // to create compound addr
    DWORD     AddressSize = 0;       // size of compound address
    LPVOID    lpConnection= NULL;    // pointer to make connection

    CoInitialize(NULL);    // registering COM

    // creating directplay object
    if  ( CoCreateInstance(CLSID_DirectPlay, NULL, CLSCTX_INPROC_SERVER,
         IID_IDirectPlay3A,(LPVOID*)&lpdp ) != S_OK)
    {
        // return  a messagebox error
        CoUninitialize();   // unregister the comp
        return(0);
    }

    // creating lobby object
    DirectPlayLobbyCreate(NULL, &old_lpdplobbyA, NULL, NULL, 0);

    // get new interface of lobby
    old_lpdplobbyA->QueryInterface(IID_IDirectPlayLobby2A, (LPVOID *)&lpdplobby));

    old_lpdplobbyA->Release();   // release old interface since we have new one

    // fill in data for address
    Address[0].guidDataType = DPAID_ServiceProvider;
    Address[0].dwDataSize   = sizeof(GUID);
    Address[0].lpData       = (LPVOID)&DPSPGUID_TCPIP;  // TCP ID

    Address[1].guidDataType = DPAID_INet;
    Address[1].dwDataSize   = lstrlen(IP_address)+1;
    Address[1].lpData       = IP_address;

    // get size to create address
    // this method will return DPERR_BUFFERTOOSMALL – not an error
    lpdplobby->CreateCompoundAddress(Address, 2, NULL, &Address_Size);

    lpConnection = GlobalAllocPtr(GHND, AddressSize);  // allocating mem

    // now creating the address
    lpdplobby->CreateCompoundAddress(Address, 2, lpConnection, &Address_Size);

    // initialize the tcp connection
    lpdp->InitializeConnection(lpConnection, 0);

    GlobalFreePtr(lpConnection);	// free allocated memory

    return(1);	// success

}	// end int Create_TCP_Connection(..)

First we initialize COM to increment its count by 1 and we create the DirectPlay object using CoCreateInstance. This is another method of creating DirectX objects, which is actually the method the wrapper function uses. We have to pass in the class identifier and such but the main thing to note is the IID_DirectPlay3A parameter. This is the identifier of the IDirectPlay3A interface. So if you want to get an IDirectPlay2A interface, set the parameter to IID_DirectPlay2A. Similarly if you want an IDirectPlay4A interface.

Then we create the DirectPlay lobby object and we query for the 2A version. Since there is a macro DirectPlayLobbyCreate, we do not need to initalize COM like above. Underneath, this function does the same (COM and CoCreateInstance) except it gets the lowest interface, ie IDirectPlayLobbyA. So we need to get the 2A version, which is done by querying it with the interface identifier (note you query all DirectX objects in the same manner). Then we close the old lobby since we have a new one.

Next we create an address that holds the information about the TCP connection. Since we did not use EnumConnections, we need to build that information ourselves. You can use the information from the enumeration and jump straight to initialize but I prefer to reduce callback functions to a minimum. We set the fields of the address structure and we set the type to the TCP/IP service provider id. This is from defined in dxguid.lib or the including INITGUID above. We set the second element to the address string. You can get all these information from the MSDN, and which parameters to pass to create other types of connection.

Then we get the size of buffer required for the connection by passing in a NULL parameter. The size required will be stored in the variable Address_Size. Note this method will return DPERR_BUFFERTOOSMALL since we are getting the size. Do not interpret this as an error condition. We then allocate memory from the system heap using GlobalAllocPtr, rather than using malloc. We then create the address information by passing the allocated buffer to the function again. Using that we call the DirectPlay Initialize method and we would have created a TCP/IP connection. If InitalizeConnection returns DPERR_UNAVAILABLE, it means the computer cannot make such a connection and it's time to inform the user no such protocol exists on the computer.

People may wonder why I chose to do things the hard way when I could have used DirectPlayCreate and be happy. Well, the main thing is I want to override the default dialog boxes that would pop up to ask the user to enter the information. You don't see that in StarCraft, do you? (Sorry, love Blizzard)

Do note that you should test every function call and return the appropriate error. I do not do that here because of space and also I'm lazy. Also as for how this function works, you pass "" as the string if you are the hosting the session, else you pass the IP of the machine you are connecting to. So in the "Getting user information", you should have enough information to call this function like:

if (gameServer)                 // if host machine
    Create_TCP_Connection("");  // empty string is enough
else
    Create_TCP_Connection(tcp_address);  // passed the global ip from user

If you think this is the end of connecting, think again. Remember, we need to have a session and a player before we send messages. But before that let's close the connection.

int	DirectPlay_Shutdown()
{
    if (lpdp)   // if connection already up, so it won't be null
    {
        if (lpdplobby)
            lpdplobby->Release();

        lpdp->Release();

        CoUninitialize();  // unregister the COM
    }

    lpdp       = NULL;  // set to NULL, safe practice here
    lpdplobby  = NULL;

    return(1);  // always success
}  // end int DirectPlay_Shutdown();

Stick this function at the close connection section.

Sessions

You will have no choice but to do a callback function here. The main functions you mainly need to do session management are:

EnumSessions      - enumerates all the session available sessions
Open              - joins or hosts a new session
Close             - close the session
GetSessionDesc    - get session properties
SetSessionDesc    - set session properties

I will only talk about the 3 functions above that we will use in this tutorial. Once you understand them, it is very easy to understand the others.

lpdp->Close();

Simple. Just close the session before you call DirectPlay_Shutdown(): All the local players created will be destroyed and the DPMSG_DESTROYPLAYERORGROUP will be sent to other players in the session.

lpdp->EnumSessions(..); // enumerates all the sessions

This function will only be called by the client side so if you are hosting the session, call Open. What is so troublesome is that the client needs to search for a session to join when they have made a connection, and since there may be more than one session in the host, we need to get every available session and present them to the user. This will require the use of a callback function. The callback function prototype is:

BOOL FAR PASCAL EnumSessionsCallback2(LPCDPSESSIONDESC2 lpThisCD,
                                      LPDWORD  lpdwTimeOut,
                                      DWORD    dwFlags,
                                      LPVOID   lpContext);

Things to note: This callback function that we implement will be called once for each session that is found using EnumSessions. Once all the sessions are enumerated, the function will be called one more time with the DPESC_TIMEOUT flag.

Any pointers returned in a callback function are only temporary and are only valid in the callback function. We must save any information we want from the enumeration. This applies to the player enumeration later. EnumSessions has the prototype:

EnumSessions(LPDPSESSIONDESC2 lpsd, DWORD dwTimeOut,
             LPDPENUMSESSIONSCALLBACK2 lpEnumSessionCallback2,
             LPVOID context, DWORD dwFlags);

If you wondering why there is a 2 behind certain typedefs, the two means the second version of the type. A rule of thumb is to always use the latest typedefs as they encapsulate more things.

The first parameter is a session descriptor so you need to initialize one.

DPSESSIONDESC2   session_desc;    // session desc

ZeroMemory(&session_desc, sizeof(DPSESSIONDESC2));  // clear the desc
session_desc.dwSize = sizeof(DPSESSIONDESC2);
session_desc.guidApplication = our_program_id;      // we define this earlier

This will ensure our program only returns sessions hosted by our program. ZeroMemory is similar to memset to 0 and as you should know, many DirectC calls require you to put the size in the structure passed.

The second parameter should be set to 0 (recommended) for a default timeout value.

The third parameter is the callback function so we pass the function to it

The fourth parameter is a user-defined context that is passed to the enumeration callback. I will describe how to use it later.

The fifth is the type of sessions to enumerate. Just pass it the default 0 meaning it will only enumerate available sessions. Check out the MSDN for more options.

All these seem to be a good candidate for a function so let's do it.

// our callback function
BOOL FAR PASCAL EnumSessionsCallback(LPCDPSESSIONDESC2 lpThisSD,
                                     LPDWORD lpdwTimeOut,
                                     DWORD dwFlags, LPVOID lpContext)
{
    HWND   hwnd;      // handle. I suggest as listbox handle

    if (dwFlags & DPESC_TIMEOUT)  // if finished enumerating stop
        return(FALSE);

    hwnd = (HWND) lpContext;   // get window handle

    // lpThisSd-> lpszSessionNameA  // store this value, name of session
    // lpThis->guidInstance         // store this, the instance of the host

    return(TRUE);  // keep enumerating
} // end callback

// our enumeration function
int EnumSessions(HWND hwnd, GUID app_guid, DWORD dwFlags)
{
    DPSESSIONDESC2   session_desc;   // session desc

    ZeroMemory(..);    // as above
    // set size of desc
    // set guid to the passed guid

    // enumerate the session. Check for error here. Vital
    lpdp->EnumSessions(&session_desc, 0, EnumSessionsCallback, hwnd, dwFlags);

    return(1);   // success
}	// end int EnumSessions

I suggest sending a listbox handle as the context so we can save the information and display it to the user. In the callback function, you must allocate space to hold the guidInstance. This is the instance of the program that is hosting the session. You need to pass this information for the user to select which one session. I have commented out the name and the instance. You save them in whatever way you want. Declare a global array and fill in the member. Whatever. I suggest a listbox so you can send messages via the hwnd parameter. Remember, the instance and name are only valid inside the callback function so you must save them to be able to present them later.

Note: If you return false in the callback, the enumeration will stop and return control the EnumSessions. If you don't stop it, it will loop forever. Also return false if you encounter an error inside. It is imperative you check the value from EnumSessions especially if you are not enumerating available sessions only. Also, the whole program will block while you are enumerating because it has to search for sessions. You can do an asynchronous enumeration too. Check it out yourself.

lpdp->Open(LPDPSESSIONDESC2 lpsd, DWORD dwFlags)

This functions hosts or joins a session using the dwFlags. If you are joining a session, you only need to fill the dwSize and guidInstance (you saved it somewhere) of the descriptor.

So we define a descriptor as:

DPSESSIONDESC2   session_desc;

ZeroMemory(&session_desc, sizeof(DPSESSIONDESC2));
session_desc.dwSize = sizeof(DPSESSIONDESC2);
session_desc.guidInstance = // instance you have save somewhere;

// and join the session
lpdp->Open(&session_desc, DPOPEN_JOIN);

If you are hosting a session, you need to fill in, in addition to the above, the name of the session, the maximum number of players allowed in the session and the session flags. You set the flag to Open as DPOPEN_CREATE | DPOPEN_RETURNSTATUS.

The return status flag hides a dialog box displaying the progress status and returns immediately. Although the documentation says I should keep calling Open with the return status flag, I have not found a need to do so (nor can I comprehend the reason they gave). You can change the session properties if you are a host using the other two methods I didn't cover.

The flags in the session desc you should set in this tutorial are:

DPSESSION_KEEPALIVE   – keeps the session alive when players are abnormally dropped.
DPSESSION_MIGRATEHOST – if the current host exits, another computer will become the host.

You can Or the flags together like so: FLAG_1 | FLAG_2 | FLAG_3. Check out the MSDN for more flag options.

Player Creation

We are just about done setting up. Now we create a local player using CreatePlayer. You have to define a name struct like so:

DPNAME    name;      // name type
DPID      dpid;      // the dpid of the player created given by directplay

ZeroMemory(&name,sizeof(DPNAME));   // clear out structure
name.size = sizeof(DPNAME);
name.lpszShortNameA = player_name;  // the name the from the user
name.lpszLongNameA = NULL;

lpdp->CreatePlayer(&dpid, &name, NULL, NULL, 0, player_flags);

This function will return a unique id for the local player within the session. Use this to identify the player rather than using the name. Save this in the local player struct. The player_name passed is obtained earlier in the information asked from user. The middle parameters are used if you do not want to poll the receive queue. Use them if you want to do multithreading. The player flags is either DPPLAYER_SERVERPLAYER or 0, which means non-server player. There can only be one server player in a session. There is also a spectator player but its meaning is defined by the application so we don't use it here.

The other function needed is EnumPlayers. I know you all are masters at enumerating now so I leave you all to implement this. Remember the global list of players we defined earlier? Just add the player inside the callback. It works the same way as the enumeration above. You don't have to enumerate the players in this chat but it is cool that you can see who is also connected at the same time.

You do not need to destroy the player because closing the session does that automatically and I don't see why you need to destroy and create another player while you are still connected. Still, it is your application.

Message Management

Now that we have a player, we need to know how to send and receive messages. I will talk about sending messages first.

Sending Messages

There is only one way to send a message and that is through the Send function. (Actually there is another if you use a lobby). If you remember, sending a message requires the id of the sender and the receiver. Good thing you have saved the local player id in a local player struct and all the players' ids in a global player list. So call the function as follows:

lpdp->Send(idFrom, idTo, dwFlags, lpData, dwDataSize);

The idFrom is the id of the local player. This must be set to a locally created player only (we don't want to impersonate another player, do we?) which in most cases is only one. The idTo is the receiver's id. Use DPID_SERVERPLAYER to send to the server only and DPID_ALLPLAYERS to send to everybody (except yourself). Note you cannot send a message to yourself. If you want to direct the message to a specific player, use the player list. You do not have to use a player list in a chat; instead you can add everything to a listbox. But later in the game, a list of players comes handy.

The flag parameter is how should the message be sent. The default is 0, which means non-guaranteed. The other options are DPSEND_GUARANTTED, DPSEND_ENCRYPTED and DPSEND_SIGNED. Sending a guaranteed message can take more than 3 times longer than a non-guaranteed one so only use guaranteed sending for important messages (like text). The signed and encrypted messages require a secure server, which we did not setup. Also any message received is guaranteed to be free of corruption (DirectPlay performs integrity checks on them).

Something to note: If you create a session that specifies no message id, their message idFrom will make no sense and the receiver will receive a message from DPID_UNKNOWN. Why anyone would want to disable message id is beyond me.

The last two parameters are a pointer to the data to send and the size of that block. Note that DirectPlay has no upper limit of the size you can send. DirectPlay will break large messages into smaller packets and reassemble them at the other end. Beware when sending non-guaranteed messages too; if one packet is lost, the whole message is discarded.

Since we are doing a chat program, I will show an example of sending a chat message

// types of messages the application will receive
const	DWORD	DP_MSG_CHATSTRING = 0;  // chat message

// the structure of a string message to send
typedef struct DP_STRING_MSG_TYP   // for variable string
{
    DWORD  dwType;     // type of message
    char   szMsg[1];   // variable length message

} DP_STRING_MSG. *DP_STRING_MSG_PTR;

// function to send string message from local player
int DP_Send_String_Mesg(DWORD type, DPID idTo, LPSTR lpstr)
{
    DP_STRING_MSG_PTR   lpStringMsg;    // message pointer
    DWORD               dwMessageSize;  // size of message
	
    // if empty string, return

    dwMessageSize = sizeof(DP_STRING_MSG)+lstrlen(lpstr); // get size

    // allocate space
    lpStringMsg = (DP_STRING_MSG_PTR)GlobalAllocPtr(GHND, dwMessageSize);

    lpStringMsg->dwType = type;          // set the type
    lstrcpy(lpStringMsg->szMsg, lpstr);  // copy the string

    // send the string
    lpdp->Send(local_player_id,idTo, DP_SEND_GUARANTEED,
        lpStringMsg, dwMessageSize);
	
    GlobalFreePtr(lpStringMsg);    // free the mem

    return(1);   // success
} // end int DP_Send_String_Mesg(..)

We first define the types of messages we can have. Since this is a chat program, there can only be one type, which I set to DP_MSG_CHATSTRING. You may add others and set the type so you can reuse the string sending function for different things. That is why the string message struct has a type to differentiate the string contents. The send function basically allocates space and sends the function to the desired player. Note the local_player_id is stored somewhere globally, or you can set it to pass another variable to set the local_player_flag. Do check the errors returned especially with allocation routines.

Receiving Messages

Receiving messages requires slightly more work than sending. There are two types of messages we can receive – a player message and a system message. A system message is sent when a change in the session state occurs. The system messages we trapped in this chat are:

DPSYS_SESSIONLOST          - the session was lost
DPSYS_HOST                 - the current host has left and you are the new host
DPSYS_CREATEPLAYERORGROUP  – a new player has join
DPSYS_DESTROYPLAYERORGROUP – a player has left

Some of those messages are only sent if certain flags are specified when then host creates the session. Consult the MSDN.

The Receive function has similar syntax to the Send function. The only different thing worth mentioning is the third parameter. Instead of the sending parameter, it is a receiving parameter. Set that to 0 for the default value, meaning extract the first message and delete it from the queue.

The whole difficult part about the receiving is that we need to cast the message to DPMSG_GENERIC and it gets messy there. So I give you the function and explain it below.

void Receive_Mesg()
{
    DPID    idFrom, idTo;         // id of player from and to
    LPVOID  lpvMsgBuffer = NULL;  // pointer to receiving buffer
    DWORD   dwMsgBufferSize;      // sizeof above buffer
    HRESULT hr;                   // temp result

    DWORD   count = 0;            // temp count of message

    // get number of message in the queue
    lpdp->GetMessageCount(local_player_id , &count);

    if (count == 0)    // if no messages
        return;        // do nothing

    do  // read all messages in queue
    {
        do  // loop until a single message is read successfully
        {
            idFrom  = 0;    // init var
            idTo    = 0;

            // get size of buffer required
            hr = lpdp->Receive(&idFrom, &idTo, 0, lpvMsgBuffer, &dwMsgBufferSize);
            if (hr == DPERR_BUFFERTOOSMALL)
            {
                if (lpvMsgBuffer)    // free old mem
                    GlobalFreePtr(lpvMsgBuffer);

                // allocate new mem
                lpvMsgBuffer = GlobalAllocPtr(GHND, dwMsgBufferSize);

            } // end if (hr ==DPERR_BUFFERTOOSMALL)
        } while(hr == DPERR_BUFFERTOOSMALL);

        // message is received in buffer
        if (SUCCEEDED(hr) && (dwMsgBufferSize >= sizeof(DPMSG_GENERIC)
        {
            if (idFrom == DPID_SYSMSG)    // if system mesg
                Handle_System_Message((LPDPMSG_GENERIC)lpvMsgBuffer,
                        dwMsgBuffersize, idFrom, idTo);
            else  // else must be application message
                Handle_Appl_Message((LPDPMSG_GENERIC)lpvMsgBuffer,
                        dwMsgBufferSize,idFrom,idTo);
        }
    } while (SUCCEEDED(hr));

    if (lpvMsgBuffer)  // free mem
        GlobalFreePtr(lpvMsgBuffer);

}  // end void Receive_Mesg()

First we check if there are any messages in the loop. If not, we break out of this function. We then keep trying to receive the message in the buffer by allocating the new buffer. When the return value is not DPERR_BUFFERTOOSMALL, it means either we have received the message or another serious error has occurred. So we check if the hresult is successful before determining whether it is a system or application message, by which we call the appropriate functions. System messages come from DPID_SYSMSG, which is a reserved value in DirectPlay.

The 2 functions should be implemented as follows:

int Handle_System_Message(LPDPMSG_GENERIC lpMsg, DWORD dwMsgSize,
                          DPID idFrom, DPID idTo)
{
    switch(lpMsg->dwType)
    {
    case DPSYS_SESSIONLOST:
        {
            // inform user
            // PostQuitMessage(0)

        } break;
    case DPSYS_HOST:
        {
            // inform user

        } break;
    case DPSYS_CREATEPLAYERORGROUP:  // a new player
        {
            // cast to get message
            LPDPMSG_CREATEPLAYERORGROUP lp = (LPDPMSG_CREATEPLAYERORGROUP)lpMsg;

            // inform user a new player has arrived
            // name of this new player is lp->dpnName.lpszShortNameA

        } break;
    case DPSYS_DESTROYPLAYERORGROUP:  // a lost player
        {
            // cast to get message
            LPDPMSG_DESTROYPLAYERORGROUP lp = (LPDPMSG_DESTROYPLAYERORGROUP)lpMsg;

            // inform user a player has left
            // name of this new player is lp->dpnName.lpszShortNameA

        } break;
    default:
        // an uncaptured message. Error here
    }  // end switch

return(1);  // success
}  // end int Handle_System_Message(..)

int Handle_Appl_Message(LPDPMSG_GENERIC lpMsg, DWORD dwMsgSize,
                        DPID idFrom, DPID idTo)
{
    switch(lpMsg->dwType)
    {
    case DP_MSG_CHATSTRING:
        {
            // cast to get the message we defined
            DP_STRING_MSG_PTR lp = (DP_STRING_MGS_PTR)lpMsg;

            if (gameServer)   // if server, relay the message to all players
		{
                lpdp->Send(local_player_id,DPID_ALLPLAYERS,
                    DPSEND_GUARANTEED, lp, dwMsgSize);
            }

            // update your chat window

        } break;
    default:
        // unknown application message, bad

    }  // end switch

    return(1);  // success
}  // end int Handle_Appl_Message(..)

The two functions are very similar. To get the actual message, we need to cast the message to the appropriate type before extracting the individual components. Even if the application message comes through, we need to cast it get the data within. The dwType parameter we defined in the chat string struct above enables us to differentiate the different messages the application defines. Although the chat requires only one message, which is the chat message, there is definitely a need for more messages types the application should handle. Every new message type you define should include a dwType parameter so the application can differentiate the messages.

Putting it Together

Now you know how it should be implemented, I will piece the various parts together (in WinMain)

// Get information from local player

// Connection creation
if (gameServer)
    Create_TCP_Connection("");
else
    Create_TCP_Connection(tcp_address);

// Session part
if (gameServer)  // if host
    // open connection
else  // if client
{
    // EnumSessions
    // Open connection
}

// Player creation
if (gameServer)  // set flags to create serverplayer
    // set flags to DPID_SERVERPLAYER
// create local player

while(1)
{
    if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)
    {
        if (msg.message == WM_QUIT)
            break;

        // translate and dispatch message
		
    }  // end if PeekMessage(..)
    else
    {
        // Your game loop

        Receive_Mesg();
    }  // end else

}  // end of while(1)

// close session

DirectPlay_Shutdown();

The reason I do not have a sample application to show is because the interface code would be as long as all this code and it would require another tutorial as long as this to explain how to do interface. However if you really do not know how to do a chat interface, I recommend looking at the control EDIT in the MSDN. For simplicity, the chat window can be implemented as a multi-line edit. Anyway I hope this makes DirectPlay clearer and the documentation makes more sense.

Discuss this article in the forums


Date this article was posted to GameDev.net: 10/5/1999
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
DirectPlay

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!