Passing Notes with DirectPlay

Kip Olson
Development Lead, DirectPlay
Microsoft Corporation

Michael Edwards
Microsoft Corporation

January 31, 1997

Click to copy the files associated with this technical article.

Contents

Introduction
Getting Connected
Getting Connected the Hard Way
Getting Connected the Easy Way
Now That You're Connected
Sending and Receiving Bytes Between Players
Remote Data
Where To Go from Here

Introduction

Games are immeasurably more compelling if they can be played against real players, and the personal computer offers richer connectivity options than ever before. Instead of leaving you to deal with the differences each of these options represents, the Microsoft® DirectPlay® application programming interface (API) for Windows® provides well-defined, generalized communication capabilities. DirectPlay shields you from the underlying complexities of diverse connectivity implementations. Applications communicate with each other independent of the underlying transport, protocol, or online service; DirectPlay provides this independence for matchmaking (lobby) servers as well. Fewer headaches in getting your application connected leaves you free to work on producing a great game.

This article is accompanied by a sample DirectPlay application, DPCHAT. DPCHAT, a text-based chat application, demonstrates how a developer can create an application with a minimum of fuss, taking advantage of the code provided by DirectPlay. DPCHAT serves to illustrate the essential elements of a typical DirectPlay application.

Getting Connected

DirectPlay encapsulates in a COM object everything you need to communicate with other players in a game session, no matter what communication channel you use. Whether these players' applications are connected via serial, modem, IPX LAN, TCP/IP LAN, or TCP/IP Internet, a single IDirectPlay2 interface serves as your means for interprocess communications. All game-session state information is also accessed through the IDirectPlay2 interface.

There are two ways to create the IDirectPlay2 object—one relatively difficult way unchanged from the days of Microsoft® DirectX® 2 and a newer, easier technique. The easy method makes use of the new game-lobby architecture added to DirectPlay 3. But here's the catch: you still have to be able to connect the hard way because you cannot be guaranteed the user will be accessing a DirectPlay 3-compatible game-lobby service.

Note   Both of these methods are illustrated with tutorials in the DirectX 3 SDK documentation.

Getting Connected the Hard Way

We just told you that the "hard" method for getting connected is not new to DirectPlay 3. We lied: the DirectPlay team made slight changes to DirectPlay2 structures. Some structures have been expanded and renamed, and the new IDirectPlay2 interface replaces the outdated IDirectPlay interface used in DirectPlay 2. What exactly is involved in getting your application connected the hard way—when it is not being launched by a game lobby?

Enumerating the Service Providers and Getting the User's Selection

First, you have to determine which DirectPlay service provider you want to use. A DirectPlay service provider is a generic layer that sits between the DirectPlay interface game developers write to and the specialized hardware and communications media that support the muliplayer aspects of your game. The DirectPlay architecture saves you from having to know the details of the communication medium by layering the core API over the service provider. DirectPlay also has built-in support for multiple service providers, allowing users to play the same game but be connected in any number of ways—via serial, modem, IPX LAN, TCP/IP LAN, or TCP/IP Internet.

The process of enumerating the providers and getting the user's selection is similar to other Win32® enumeration techniques, such as that used for enumerating fonts. You make a system call, providing a pointer to a function that will be called back synchronously. Each call provides you with one item that you add to a list box or other user-interface control. In this case, the enumeration provides a user-readable name identifying the service provider as well as a 32-bit magic cookie. The magic cookie is a pointer to the provider's GUID, or globally unique identifier. Each application and service provider must have a GUID—this is simply a unique 128-bit value that is created during the development process to identify a particular component. DirectPlay also dynamically generates and assigns a GUID to every new game session. Thus, no two service providers, or game sessions, or DirectPlay applications, have the same GUID. The DirectPlay SDK provides a tool called GUIDGEN to generate GUIDs that are guaranteed to be unique for all time. For more information on GUIDs, see "Globally Unique Identifiers (GUIDS)" in the MSDN™ Library, book section.

Take a look at the code in DPCHAT (from the dpchat.h and dialog.cpp files) that creates the connection dialog box. Note that in the process of creating the dialog box, the DPLAYINFO structure is initialized. A pointer is passed to DPLAYINFO as a parameter to ConnectUsingDialog(). An IDirectPlay2 interface, lpDirectPlay2A, is created, bound to the service provider, and connected to the game session. Users chat with other players using lpDirectPlay2A in the Chat dialog box. The player event handle, HPLAYEREVENT, is used to synchronize with DirectPlay for receiving messages in a separate thread. dpidPlayer uniquely identifies the local player, and bIsHost indicates whether the player joined an existing, or created a new, session.

typedef struct {
 LPDIRECTPLAY2A lpDirectPlay2A; // IDirectPlay2A interface pointer
 HANDLE   hPlayerEvent; // player event to use
 DPID    dpidPlayer;  // ID of player created
 BOOL    bIsHost;  // TRUE if we are hosting the session.
} DPLAYINFO, *LPDPLAYINFO;
 
HRESULT ConnectUsingDialog(HINSTANCE hInstance, LPDPLAYINFO lpDPInfo)
{
 // Ask user for connection settings.
 if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_CONNECTDIALOG),
        NULL, (DLGPROC) ConnectWndProc, (LPARAM) lpDPInfo))
 {
  return (DP_OK);
 }
 else
 {
  return (DPERR_USERCANCEL);
 }
}

The IDD_CONNECTDIALOG dialog box looks like the following one in Figure 1:

Figure 1. Example of an IDD_CONNECTDIALOG dialog box

The five dialog-box controls are as follows: at the top of the dialog is the IDC_SPCOMBO box containing text descriptions of each service provider; the IDC_SESSIONLIST box appearing immediately below contains the game sessions available through the selected service provider (as you can see above, one TCP/IP chat session is available—"MICHAELE"); the three buttons at the bottom of the dialog give the user the options to Host, or create, a new game session with the selected service provider; Join the selected game session (assuming one has been found, otherwise the option is disabled); or Search for game sessions available through the selected service provider (currently active sessions are then added to the IDC_SESSIONLIST box).

Now look at the relevant pieces of the window procedure for the IDD_CONNECTDIALOG dialog box. When the connection dialog box is initialized, the service providers are enumerated into the IDC_SPCOMBO box control:

BOOL CALLBACK ConnectWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 static LPDPLAYINFO  lpDPInfo;
 static LPDIRECTPLAY2A lpDirectPlay2A;
 GUID     guidServiceProvider;
 GUID     guidSessionInstance;
 char     szSessionName[MAXNAMELEN];
 char     szPlayerName[MAXNAMELEN];
 DWORD     dwNameSize;
 HRESULT    hr;

    switch(uMsg)
    {
    case WM_INITDIALOG:
      // Save the connection info pointer.
        lpDPInfo = (LPDPLAYINFO) lParam;
  lpDirectPlay2A = NULL;

  // Put all the service providers in a combo box.
  DirectPlayEnumerate(DirectPlayEnumerateCallback, hWnd);
  SendDlgItemMessage(hWnd, IDC_SPCOMBO, CB_SETCURSEL, (WPARAM) 0, (LPARAM) 0);

  // Set up initial button state.
  EnableDlgButton(hWnd, IDC_HOSTBUTTON, TRUE);
  EnableDlgButton(hWnd, IDC_JOINBUTTON, FALSE);
  EnableDlgButton(hWnd, IDC_ENUMERATEBUTTON, TRUE);
      break;

DirectPlayEnumerate( ) is the DirectPlay system call that enumerates all the service providers. The callback routine that DPCHAT supplies—DirectPlayEnumerateCallback( )—receives the dialog's window handle as the application's context data (in order to access the IDC_SPCOMBO box). The callback routine is invoked once per service provider. The callback adds the name of each provider to the IDC_SPCOMBO box and caches the provider's GUID with each entry. You might think the text name is enough to distinguish the provider, but that name is for the user's benefit only; DirectPlay uses the service provider's GUID. The enumeration is pretty straightforward—typical of the code you would use, for example, to stuff a list box or combo box with text entries and an associated 32-bit value:

BOOL FAR PASCAL DirectPlayEnumerateCallback(
                      LPGUID      lpSPGuid,
                      LPTSTR      lpszSPName,
                      DWORD       dwMajorVersion,
                      DWORD       dwMinorVersion,
                      LPVOID      lpContext)
{
   HWND   hWnd = (HWND) lpContext;
   LRESULT  iIndex;
 LPGUID  lpGuid;

 // Store service provider name in combo box.
   iIndex = SendDlgItemMessage(hWnd, IDC_SPCOMBO, CB_ADDSTRING, 
                               0, (LPARAM) lpszSPName);
   if (iIndex == CB_ERR)
      goto FAILURE;

   // Make space for application GUID pointer.
   lpGuid = (LPGUID) GlobalAllocPtr(GHND, sizeof(GUID));
   if (lpGuid == NULL)
      goto FAILURE;

   // Store pointer to GUID in combo box.
   *lpGuid = *lpSPGuid;
   SendDlgItemMessage(hWnd, IDC_SPCOMBO, CB_SETITEMDATA,
                     (WPARAM) iIndex, (LPARAM) lpGuid);
FAILURE:
   return (TRUE);
}

Enumerating the Application's Current Sessions. The DirectPlay architecture supports individual game sessions through a single service provider only. Although multiple, concurrent sessions of a given DirectPlay-compatible game can be running using any number of service providers, all players in a given session must use the same service provider. This architecture is reflected in the way a user selects a game session using DPCHAT. When the user changes the service-provider selection in the IDC_SPCOMBO box, the IDC_SESSIONLIST box (which lists available game sessions) is cleared because it contains the sessions from the previously selected service provider. Also, because the IDirectPlay2 interface is tied to the previous provider, it must be released as well. This is shown in the CBN_SELCHANGE message handling of ConnectWndProc( ). DestroyDirectPlayInterface( ) is a simple procedure (which you can refer to in dialog.cpp) that clears the list of sessions in the IDC_SESSIONLIST box (after freeing the global memory for the session GUIDs) and calls the Release method on the IDirectPlay2 interface:

case WM_COMMAND:
      switch(LOWORD(wParam))
         {
      case IDC_SPCOMBO:
         switch (HIWORD(wParam))
         {
         case CBN_SELCHANGE:
            // Service provider changed, so clear display and
            // delete any existing DirectPlay interface.
            hr = DestroyDirectPlayInterface(hWnd, lpDirectPlay2A);
            lpDirectPlay2A = NULL;
            break;
         }
         break;

Session entries are not added to the IDC_SESSIONLIST box until the user presses the IDC_ENUMERATEBUTTON (Search) button. Enumerating game sessions is a two-part process: first create the IDirectPlay2 interface from the service provider GUID, then use that interface to enumerate the game sessions currently online with that service provider.

case IDC_ENUMERATEBUTTON:

   // DirectPlay interface has not been created yet.
   if (lpDirectPlay2A == NULL)
      {
      // Get guid of selected service provider.

Recall we already cached the GUID for each service provider in the IDC_SPCOMBO box, and so GetServiceProviderGuid() is a simple helper function in dialog.cpp that retrieves this unique 128-bit value.

    hr = GetServiceProviderGuid(hWnd, &guidServiceProvider);
    if FAILED(hr)
       goto ENUMERATE_FAILURE;

       // Create a DirectPlay interface using this service provider,
         hr = CreateDirectPlayInterface(&guidServiceProvider, 
                                        &lpDirectPlay2A);
         if FAILED(hr)
            goto ENUMERATE_FAILURE;
      }

   // Dim button while enumerating.
   EnableDlgButton(hWnd, IDC_ENUMERATEBUTTON, FALSE);

   // Enumerate sessions of the service provider (this will take a little while).
   hr = EnumSessions(hWnd, lpDirectPlay2A);

   EnableDlgButton(hWnd, IDC_ENUMERATEBUTTON, TRUE);

   if FAILED(hr)
      goto ENUMERATE_FAILURE;

   break;

ENUMERATE_FAILURE:
   ErrorBox("Could not enumerate sessions because of error 0x%08X", hr);

   hr = DestroyDirectPlayInterface(hWnd, lpDirectPlay2A);
   lpDirectPlay2A = NULL;
   break;

Creating the IDirectPlay2 interface is fairly straightforward, especially if you're familiar with COM programming. The only real twist here is that the DirectPlayCreate function returns the old IDirectPlay interface. You will need to do another setup to get the new IDirectPlay2 interface.

HRESULT CreateDirectPlayInterface(LPGUID lpguidServiceProvider,
        LPDIRECTPLAY2A *lplpDirectPlay2A)
{
 LPDIRECTPLAY  lpDirectPlay1 = NULL;
 LPDIRECTPLAY2A lpDirectPlay2A = NULL;
 HRESULT   hr;

 // Get a DirectPlay 1.0 interface.
 hr = DirectPlayCreate(lpguidServiceProvider, &lpDirectPlay1, NULL);
 if FAILED(hr)
  goto FAILURE;

 // Query for an ANSI DirectPlay2 interface.
 hr = lpDirectPlay1->QueryInterface(IID_IDirectPlay2A, 
            (LPVOID *) &lpDirectPlay2A);
 if FAILED(hr)
  goto FAILURE;

 // Return interface created.
 *lplpDirectPlay2A = lpDirectPlay2A;

FAILURE:
 if (lpDirectPlay1)
  lpDirectPlay1->Release();

 return (hr);
}

Enumerating the available game sessions, the last step in this process, is worth discussing in a little more detail.

Take a look at the code above, and notice that the settings passed in the DPSESSIONDESC2 structure determine which game sessions are enumerated using the IDirectPlay2 interface. These settings allow the application to discriminate from among all possible sessions. By passing NULL, you can use the guidInstance field to specify that all sessions on the service provider be enumerated. You could also fill in this value with a pointer to the GUID of a specific session in order to get updated parameters for that session (session parameters are dynamic beasts; the data you get is out of date as soon as soon as you get it).

Note   Unfortunately, a DirectPlay bug makes this option unavailable at present; so if you try this at home, you will get the same cached parameters.

Generally speaking, you'll be interested in enumerating only your own application sessions, which means you'll be filling in the guidApplication pointer with your application's GUID. In this case, you would specify enumerating all sessions on the service provider by passing NULL in the guidInstance field. For example, a user might be interested in finding available game sessions for more than one application -- a lobby client might be looking for all applications registered as lobbyable on the user's machine. DPCHAT fills this field in with a pointer to its own GUID in order to see only DPCHAT sessions.

Use the bits in the dwFlags field either to specify the sessions that can be joined or to indicate all sessions, whether or not they can be joined. If you specify DPENUMSESSIONS_AVAILABLE, DirectPlay will validate the application and session GUIDs (if you supplied them); it will make sure that the current number of users is less than the maximum allowed; it will determine whether the passwords (if applicable) match; and it will determine whether the session can be joined. If you specify DPENUMSESSIONS_ALL, all DPSESSIONDESC2 parameters are ignored. This might be your only option if, for instance, your application allows for password-protected sessions: you wouldn't be able to enumerate them using DPENUMSESSIONS_AVAILABLE unless you had supplied the password in the first place, and even then you would only get the sessions matching that password (of course you will not be able to join a password-protected session without supplying the password).

After filling in the DPSESSIONDESC2 structure, you are ready to enumerate the available sessions:

HRESULT EnumSessions(HWND hWnd, LPDIRECTPLAY2A lpDirectPlay2A)
{
 DPSESSIONDESC2 sessionDesc;
 LONG    iIndex;
 HRESULT   hr;

 // Check for valid interface.
 if (lpDirectPlay2A == NULL)
  return (DPERR_INVALIDOBJECT);

 // Delete existing list.
 DeleteSessionInstanceList(hWnd);

 // Search for this kind of session.
 ZeroMemory(&sessionDesc, sizeof(DPSESSIONDESC2));
 sessionDesc.dwSize = sizeof(DPSESSIONDESC2);
    sessionDesc.guidApplication = DPCHAT_GUID;

 hr = lpDirectPlay2A->EnumSessions(&sessionDesc, 0, EnumSessionsCallback,
           hWnd, DPENUMSESSIONS_AVAILABLE);
 if FAILED(hr)
  goto FAILURE;

 // Select first item in list.
 SendDlgItemMessage(hWnd, IDC_SESSIONLIST, LB_SETCURSEL,
         (WPARAM) 0, (LPARAM) 0);

 // Hilite "Join" button if there are sessions to join.
 iIndex = SendDlgItemMessage(hWnd, IDC_SESSIONLIST, LB_GETCOUNT,
         (WPARAM) 0, (LPARAM) 0);

 EnableDlgButton(hWnd, IDC_JOINBUTTON, (iIndex == 0) ? FALSE : TRUE);

FAILURE:
 return (hr);
}

Before the EnumSessions method call returns, you'll see a dialog box that does not appear anywhere in DPCHAT's resource file (dpchat.rc). This dialog box is displayed by the service provider when DirectPlay asks for the sessions. Because each service provider needs different parameters from the user, it makes sense to have this detail reside with the provider. Note that you can't override the creation of this dialog box from your DirectPlay application. This dialog box always appears unless you are launched by a lobby. The TCP/IP provider displays the following dialog box in Figure 2:

Figure 2. Example of Locate Session dialog box

The user can leave the edit field blank to locate all sessions on the local subnet (though this generally works only on LANs). If the user wants to locate a particular session on the Internet, he or she must enter the specific IP address or computer name of a machine known to be hosting a game session.

Take a look now at the enumeration callback function—a function not unlike that used in enumerating available service providers. In this case, however, DirectPlay provides you with session names; the 32-bit application-specific data you are caching away with each name is different (this time, it is the session GUID assigned by DirectPlay when the session was created); and you are adding these names to the IDC_SESSIONLIST box instead of the IDC_SPCOMBO box. Review the code before taking a more detailed look at what is new here:

BOOL FAR PASCAL EnumSessionsCallback(
      LPCDPSESSIONDESC2 lpSessionDesc,
      LPDWORD  lpdwTimeOut,
      DWORD   dwFlags,
      LPVOID  lpContext)
{
 HWND   hWnd = (HWND) lpContext;
 LPGUID  lpGuid;
 LONG   iIndex;

 // See if enum has timed out.
    if (dwFlags & DPESC_TIMEDOUT)
  return (FALSE);      // don't try again

 // Store session name in list.
 iIndex = SendDlgItemMessage( hWnd, IDC_SESSIONLIST, LB_ADDSTRING, 
        (WPARAM) 0, 
        (LPARAM) lpSessionDesc->lpszSessionNameA);
 if (iIndex == CB_ERR)
  goto FAILURE;

 // Make space for session instance guid.
 lpGuid = (LPGUID) GlobalAllocPtr(GHND, sizeof(GUID));
 if (lpGuid == NULL)
  goto FAILURE;

 // Store pointer to guid in list.
 *lpGuid = lpSessionDesc->guidInstance;
 SendDlgItemMessage(hWnd, IDC_SESSIONLIST, LB_SETITEMDATA, 
      (WPARAM) iIndex, (LPARAM) lpGuid);
FAILURE:
    return (TRUE);
}

dwTimeOut and DPESC_TIMEDOUT take into account the fact that the application should limit the amount of time DirectPlay searches for hosts. DirectPlay can find and communicate with any number of hosts, any one of which introduces a latency of unpredictable duration. In fact, session enumeration always times out: a DirectPlay application is never "finished" enumerating game sessions but merely enumerates as many as possible in the time allotted. The actual time-out value varies with the service provider; ideally, it is long enough for the user to find all of the sessions without being sucked into a black hole caused by a problem contacting a given host. DirectPlay spends the specified time interval enumerating sessions and then calls it a day. Of course, in the spirit of DirectX, if you don't trust the service provider to use a good time-out value (by passing 0 for dwTimeOut in EnumSessions), you can specify one yourself. A longer wait may mean more hosts are found, but also increasing frustration with diminishing returns for the user.

lpSessionDesc provides different sorts of information, depending upon what you were looking for in the first place. For example, DPCHAT enumerates on the application GUID because it is only interested in finding sessions of DPCHAT. DPCHAT needs only the session name (for the user) and the session GUID (to connect to the session). But your application may have supplied the session GUID, if you are trying to find out whether joining is still enabled or whether the session is full. Or maybe you passed in a null application GUID, to find all the applications currently online. Be sure to make a copy of this structure because the memory is valid only for the duration of each callback.

Note   You cannot use an IDirectPlay2 interface to enumerate sessions after you've already used it to create or join one. That partially explains why the interface is destroyed when the selection in the service provider box is changed. Another reason is because the process is dynamic in nature—sessions may have been created and destroyed that you wouldn't know about unless you enumerated them again.

Hosting a New Session or Joining One. Now that the user has a list of available sessions, he or she is almost ready to start chatting. Even if there aren't any DPCHAT sessions on the net, the user can still get online by pressing the Host button to create a session.

Hosting a Session. The Host button is always enabled, unlike the Join button, which is enabled only if at least one session has been enumerated that can be joined. If the user chooses to enumerate before deciding to host, then the IDirectPlay2 interface is already available. If not—if the first thing the user did was click the Host button—we don't yet have an IDirectPlay2 interface; creating one is the intent of the first few lines of the IDC_HOSTBUTTON handling in ConnectWndProc(). The next couple of lines shows you why the session you saw in the Choose A Session dialog box was named MICHAELE: DPCHAT names sessions after the registered computer name of the host's system. DPCHAT uses the name of the user to name the initial player in the session and passes both these names, along with the IDirectPlay2 interface pointer, to the helper function HostSession( ).

The following code sets things up a bit before the "real" work for creating a session is taken up in HostSession( ):

case IDC_HOSTBUTTON:

 // DirectPlay interface has not been created yet.
 if (lpDirectPlay2A == NULL)
 {
  // Get guid of selected service provider.
  hr = GetServiceProviderGuid(hWnd, &guidServiceProvider);
  if FAILED(hr)
   goto HOST_FAILURE;

  // Create a DirectPlay interface using this service provider,
  hr = CreateDirectPlayInterface(&guidServiceProvider, &lpDirectPlay2A);
  if FAILED(hr)
   goto HOST_FAILURE;
 }

 // Use computer name for session name.
 dwNameSize = MAXNAMELEN;
 if (!GetComputerName(szSessionName, &dwNameSize))
  strcpy(szSessionName, "Session");

 // Use user name for player name.
 dwNameSize = MAXNAMELEN;
 if (!GetUserName(szPlayerName, &dwNameSize))
  strcpy(szPlayerName, "unknown");

 // Host a new session on this service provider.
 hr = HostSession(lpDirectPlay2A, szSessionName, szPlayerName, lpDPInfo);
 if FAILED(hr)
  goto HOST_FAILURE;

 // Dismiss dialog if we succeeded in hosting.
 EndDialog(hWnd, TRUE);
 break;

HOST_FAILURE:
 ErrorBox("Could not host session because of error 0x%08X", hr);

 hr = DestroyDirectPlayInterface(hWnd, lpDirectPlay2A);
 lpDirectPlay2A = NULL;
 break;

Here is the code for HostSession( ). Take a look at it before we explain in more detail what's happening:

HRESULT HostSession(LPDIRECTPLAY2A lpDirectPlay2A,
     LPSTR lpszSessionName, LPSTR lpszPlayerName,
     LPDPLAYINFO lpDPInfo)
{
 DPID    dpidPlayer;
 DPNAME   dpName;
 DPSESSIONDESC2 sessionDesc;
 HRESULT   hr;

 // Check for valid interface.
 if (lpDirectPlay2A == NULL)
  return (DPERR_INVALIDOBJECT);

 // Host a new session.
 ZeroMemory(&sessionDesc, sizeof(DPSESSIONDESC2));
 sessionDesc.dwSize = sizeof(DPSESSIONDESC2);
   sessionDesc.dwFlags = DPSESSION_MIGRATEHOST | DPSESSION_KEEPALIVE;
   sessionDesc.guidApplication = DPCHAT_GUID;
   sessionDesc.dwMaxPlayers = MAXPLAYERS;
 sessionDesc.lpszSessionNameA = lpszSessionName;

 hr = lpDirectPlay2A->Open(&sessionDesc, DPOPEN_CREATE);
 if FAILED(hr)
  goto OPEN_FAILURE;

 // Fill out name structure.
 ZeroMemory(&dpName, sizeof(DPNAME));
 dpName.dwSize = sizeof(DPNAME);
 dpName.lpszShortNameA = lpszPlayerName;
 dpName.lpszLongNameA = NULL;

 // Create a player with this name.
 hr = lpDirectPlay2A->CreatePlayer(&dpidPlayer, &dpName, 
       lpDPInfo->hPlayerEvent, NULL, 0, 0);
 if FAILED(hr)
  goto CREATEPLAYER_FAILURE;

 // Return connection info.
 lpDPInfo->lpDirectPlay2A = lpDirectPlay2A;
 lpDPInfo->dpidPlayer = dpidPlayer;
 lpDPInfo->bIsHost = TRUE;

 return (DP_OK);

CREATEPLAYER_FAILURE:
OPEN_FAILURE:
 lpDirectPlay2A->Close();
 return (hr);
}

The first thing we do is initialize the session parameters described in the DPSESSIONDESC2 structure. Remember to initialize to zero the DPSESSIONDESC2 fields that are not of interest. For example, a non-zero guidInstance value would be invalid in combination with the DPOPEN_CREATE flag, and DirectPlay would fail the Open method. The session attributes used by DPCHAT make sense for a chat application: DPSESSION_MIGRATEHOST passes the responsibility for hosting to another user to keep the chat session alive if a host bails out or loses his connection; and the DPSESSION_KEEPALIVE flag instructs DirectPlay to maintain a check on the connections of remaining users. Rather than continuing to retry packets to a player whose connection is hosed, or continuing to throw nonguaranteed packets into the ether, DirectPlay pulls the plug on the terminal patient. DPSESSION_KEEPALIVE works locally as well: DirectPlay sends a system message to the application if its connection has been lost. Whether the application cuts off life support at that time is up to you, but there is no way to rejoin the session as the same player.

DPCHAT initializes the next DPSESSIONDESC2 parameter—the type of application session (a DPCHAT session)—by assigning the application GUID to the guidApplication field. Then the application specifies a maximum number of players allowed in the session and assigns the session name.

Once the Open method is completed, the session has been created with the service provider using the parameters initialized in the DPSESSIONDESC2 structure. Other people using the same service provider will now see your session when they do a search.

Then, with a new session in place, the user can be added, thereby creating the initial player identity. At this point, the application gives DirectPlay an event handle if it is important to have an event signaled when a message arrives for player identification. This will be discussed further when we talk about sending and receiving messages.

Finally, DPCHAT initializes the global DPLAYINFO structure with the IDirectPlay2 interface pointer as well as the player's ID and the host flag.

Joining a Session. The JoinSession( ) code closely resembles the HostSession( ) code. The difference between the two is that the application doesn't need to establish session attributes because it is not creating a session. Rather than leave the guidInstance value blank, DPCHAT has to fill it in with the GUID for the session being joined. Also, DPCHAT uses the DPOPEN_JOIN flag (rather than DPOPEN_CREATE) in the Open call.

As you might expect, the message-handling code in ConnectWndProc( ) for the IDC_JOINBUTTON button, which invokes JoinSession( ), closely resembles that for IDC_HOSTBUTTON that invokes HostSession( ). The difference is that the former does not obtain a machine name for naming the session, and it uses the helper function GetSessionInstanceGuid( ) to get the GUID associated with the selection in the IDC_SESSIONLIST box.

If the user has never pressed the IDC_ENUMERATEBUTTON (Search) button to search for sessions, or if he or she did but no sessions were found, the Join button remains disabled.

Getting Connected the Easy Way

Although it may appear that getting connected the hard way is actually not all that difficult, a fair bit of coding is involved. You also have to deal with a lot of user-interface issues—which means there are lots of places where things can go wrong. Designing and debugging interaction with the user is not fun or easy: people do the weirdest things, and what you considered simple isn't always as simple as you think. Complicating things further is the fact that most people haven't a clue what an IP address is, much less how to figure out what theirs (or anyone else's) is.

Connecting the hard way yields an IDirectPlay2 interface to a game session chosen by (or created by) the user, along with the user's player ID. This is encapsulated in the global LPDPLAYINFO structure that is filled out in the following code from SetupConnection( ) in dpchat.cpp. Note that this code starts you down the path of making the application lobbyable—making it aware that it may have been launched from a DirectPlay game lobby. The function ConnectUsingLobby( ) checks to see if the application was launched by a game lobby; failing that, the application must handle the connection itself (must connect, in other words, the hard way). Either way you end up with the same information.

// Try to connect using the lobby.
hr = ConnectUsingLobby(lpDPInfo);
if FAILED(hr)
{
 // If the error returned is DPERR_NOTLOBBIED, that means we
 // were not launched by a lobby and we should ask the user for
 // connection settings. If any other error is returned, it means
 // the app was launched by a lobby but there was an error making the
 // connection.

 if (hr != DPERR_NOTLOBBIED)
  ErrorBox("Could not connect using lobby because of error 0x%08X", hr);

 // If there is no lobby connection, ask the user for settings.
 hr = ConnectUsingDialog(hInstance, lpDPInfo);
 if FAILED(hr)
  goto FAILURE;
}

Making Your Application Lobbyable. You've no doubt seen a game lobby by now—Kali95, perhaps, or one of the new online game services. Basically, the DirectPlay game lobby replaces every bit of code we've covered up to this point. The DirectPlay game-lobby architecture keeps the entire user interface for choosing online play separate from your game. The game-lobby function could be done by a Windows 95 application running on the user's local machine and connected to a third-party online service provider. Or, the game lobby could be implemented as a server on a web site from which the user downloads the client-side application for choosing a game session. Or, the lobby-client application might reside on a web site using ActiveX® controls. The intent is to let somebody else make a business out of getting people connected. That saves you having to do it five different ways for five different online providers (or give some online provider access to your sources to do it for you). For more information on the game lobby feature in DirectPlay 3, see "DirectPlay3: Let the Online Gaming Begin" at www.microsoft.com/directx/pavilion/dplay/dplay3.htm. The DirectX 3 SDK Help file also covers this feature.

Communication between the game and the lobby client is done via a new DirectPlay3 COM interface called IDirectPlayLobby. You create an instance of this interface through a new DirectPlay function called DirectPlayLobbyCreate( ). With this interface, you can query and modify the connection settings as well as obtain an IDirectPlay2 interface connected to the game session.

Note   You must make changes to the connection settings before hooking up to the game session; you can't change them afterward in DirectPlay3 (look for this to be fixed in the next release). This process is demonstrated in DPCHAT function ConnectUsingLobby( ):

HRESULT ConnectUsingLobby(LPDPLAYINFO lpDPInfo)
{
 LPDIRECTPLAY2A  lpDirectPlay2A = NULL;
 LPDIRECTPLAYLOBBYA  lpDirectPlayLobbyA = NULL;
 LPDPLCONNECTION  lpConnectionSettings = NULL;
 DPID     dpidPlayer;
 DWORD     dwSize;
 HRESULT    hr;

 // Get an ANSI DirectPlay lobby interface.
 hr = DirectPlayLobbyCreate(NULL, &lpDirectPlayLobbyA, NULL, NULL, 0);
 if FAILED(hr)
  goto FAILURE;

   // Get connection settings from the lobby.
 // If this routine returns DPERR_NOTLOBBIED, then a lobby did not
 // launch this application, and the user needs to configure the connection.

 // Pass in a NULL pointer to just get the size of the connection settings.
 hr = lpDirectPlayLobbyA->GetConnectionSettings(0, NULL, &dwSize);
 if (DPERR_BUFFERTOOSMALL != hr)
  goto FAILURE;

 // Allocate memory for the connection settings.
 lpConnectionSettings = (LPDPLCONNECTION) GlobalAllocPtr(GHND, dwSize);
 if (NULL == lpConnectionSettings)
 {
  hr = DPERR_OUTOFMEMORY;
  goto FAILURE;
 }

 // Get the connection settings.
 hr = lpDirectPlayLobbyA->GetConnectionSettings(0, lpConnectionSettings,
           &dwSize);
 if FAILED(hr)
  goto FAILURE;

 // Before connecting, the game should configure the session description
 // with any settings it needs.

 // Set flags and max players used by the game.
   lpConnectionSettings->lpSessionDesc->dwFlags = DPSESSION_MIGRATEHOST | 
               DPSESSION_KEEPALIVE;
   lpConnectionSettings->lpSessionDesc->dwMaxPlayers = MAXPLAYERS;

   // Store the updated connection settings.
   hr = lpDirectPlayLobbyA->SetConnectionSettings(0, 0, lpConnectionSettings);
 if FAILED(hr)
  goto FAILURE;

 // Connect to the session - returns an ANSI IDirectPlay2A interface.
 hr = lpDirectPlayLobbyA->Connect(0, &lpDirectPlay2A, NULL);
 if FAILED(hr)
  goto FAILURE;

 // Create a player with the name returned in the connection settings.
 hr = lpDirectPlay2A->CreatePlayer(&dpidPlayer,
       lpConnectionSettings->lpPlayerName, 
       lpDPInfo->hPlayerEvent, NULL, 0, 0);
 if FAILED(hr)
  goto FAILURE;

 // Return connection info.
 lpDPInfo->lpDirectPlay2A = lpDirectPlay2A;
 lpDPInfo->dpidPlayer = dpidPlayer;
 if (lpConnectionSettings->dwFlags & DPLCONNECTION_CREATESESSION)
  lpDPInfo->bIsHost = TRUE;
 else
  lpDPInfo->bIsHost = FALSE;

 lpDirectPlay2A = NULL; // Set to NULL here so it won't release below.

FAILURE:
 if (lpDirectPlay2A)
  lpDirectPlay2A->Release();

 if (lpDirectPlayLobbyA)
  lpDirectPlayLobbyA->Release();

 if (lpConnectionSettings)
  GlobalFreePtr(lpConnectionSettings);

 return (hr);
}

There are also methods in IDirectPlayLobby for exchanging private data with the lobby-client application. Of course, this can be done only through a cooperating lobby-client application—you have to determine the nature of this data with your DirectPlay-compatible lobby service providers before releasing your application (unless the provider knows what you want and can't very well provide it for you).

Reading about the IDirectPlayLobby interface in the DirectX 3 SDK documentation may be confusing. Remember that the IDirectPlayLobby interface is implemented by DirectPlay, and an instance of that interface is created both by the game and the lobby-client application. The documentation is written for the developers who will use this interface to implement a lobby client as well as those developing lobbyable games.

The process of launching a lobbyable game is roughly as follows: the user starts the lobby-client software, which presents an interface meant to show which games are available for play (different online game providers will make different games available due to, for example, exclusive deals), and the user chooses to join an existing session or participates in creating a new one. After the user makes his or her game selection, the lobby-client creates an IDirectPlayLobby interface and looks in the Windows 95 registry to get the information needed to launch the indicated game. Using that interface, the lobby client sends DirectPlay all the data needed by the application to connect to the indicated session and then instructs DirectPlay to launch the application. When the application starts up, it determines that it was launched from a lobby client and creates its own IDirectPlayLobby interface, which it can use to obtain the connection settings and get connected without involving the user beyond his or her initial interaction with the lobby client.

Getting Registered as a Lobbyable Application. One more task: you must update the Windows 95 registry during the application setup process in order to let DirectPlay know that your game is available for online play using a lobby. The following registry keys have been defined for this purpose. You can use the DirectXRegisterApplication function to add these entries.

[HKEY_LOCAL_MACHINE\Software\Microsoft\DirectPlay\Applications\Application Name]
"Guid"    GUID of the application
"Filename"   Filename of the executable
"CommandLine"  Command-line switches for the application (if any)
"Path"    Path of application executable
"CurrentDirectory" Path of directory to launch application into.

The DPCHAT sample uses the registration file called dpchat.reg, although you will need to modify the Path field to match your install location.

Testing Whether Your Application Is Lobbyable. Finally, you must test whether or not you are really lobbyable. Presumably, you are working with an online game provider, but most likely you would prefer not to appear uninformed. In fact, you are hoping to impress the provider with your DirectPlay expertise. A new sample application in the DirectX 3 SDK—DPLAUNCH—implements a rudimentary lobby client that will start you on testing. DPLAUNCH.EXE is available in the SDK/BIN directory, already built, and the sources are in the SDK/SAMPLE directory. Of course, you'll also get a chance to bone up on your cross-application debugging.

Now That You're Connected

If you've followed me up to this point, you now understand that the IDirectPlay2 interface is the key to interprocess communication with all other instances of your application. With it you can:

Now you're ready for the fun part—interprocess communications, or sending and receiving information between the instances of your application running here, there, and everywhere.

Sending and Receiving Bytes between Players

Presumably you know the exact information you need to exchange with the other instances of your application, and you have a plan in hand for accomplishing this mission—because helping you figure out these things is beyond the scope of this article. The first and most important task is to have a well-thought-out and correct approach for exchanging data between your application instances. The next DirectPlay article will cover in some detail the problems inherent in writing real-time Internet applications; for now you're on your own.

Fortunately, it is doesn't take a rocket scientist to figure out what data needs to be exchanged between instances of DPCHAT; it is more or less a matter of sending and posting text strings. What we're hoping is that, without getting into the problem of what data you should be exchanging, you will easily see how you exchange data with DirectPlay.

If you were successful in hosting or joining a DPCHAT session, you saw the final piece of user interface in DPCHAT that does the actual chatting. This simple dialog box has three controls—an edit box for typing new messages to be sent, a list box to display the previous messages sent and received, and a button to send messages entered in the edit box. Figure 2 provides an example of a session that has been joined by michaele (who also sent the "hi michaele" message).

Figure 3. Example of DirectPlay Chat dialog box

Sending

DPCHAT uses a function called SendChatMessage( ) to send application messages. This function is invoked by the DirectPlay Chat dialog window procedure, ChatWndProc( ) in dpchat.cpp, when the user presses the Send button. Nearly all DPCHAT sample code in SendChatMessage( ) is typical of what you'd see working with text strings in Win32 dialog box controls. Only two small pieces use DirectPlay to actually send the text data. To simplify, we will use some pseudocode to summarize what is going on in this routine.

// Get the text data they want to send from the edit control.
lpChatText = blah blah

// Get the player's name from their playerid.
hr = GetPlayerName(lpDirectPlay2A, dpidPlayer, &lpName);
if FAILED(hr)
 goto FAILURE;

// Format the chat text into a string with the player's name displayed
// in front of it.
lpChatMessage = blah blah

// Send this string to all other players.
hr = lpDPInfo->lpDirectPlay2A->Send(lpDPInfo->dpidPlayer, DPID_ALLPLAYERS,
         DPSEND_GUARANTEED, lpChatMessage,
         dwChatMessageSize);
if FAILED(hr)
 goto FAILURE;

// Display the string locally.
blah blah

// Clear the edit control of the sent message.
blah blah

FAILURE:

// Clean up and bail out of here.

DPCHAT broadcasts its chat messages to all other players by indicating the DPID_ALLPLAYERS value for the target player ID in the Send method. Your application can just as easily send messages to a specific player or group of players. The DPID_ALLPLAYERS flag broadcasts the message to every player in the session; to send to a single user, substitute that player's ID. To send to a group of players, use the group-management capabilities of DirectPlay to create the group and its group ID. Creating a group of players may require additional user interface and perhaps some different sorts of data structures, but sending the group a message is no more difficult than indicating a group ID value instead of a player id.

Need a Guarantee? Notice that DPCHAT uses guaranteed messaging. Although a detailed discussion of the relative merits of guaranteed vs. nonguaranteed messaging is outside the scope of this article, it is worth explaining why we have chosen to use guaranteed messaging here.

What does "guaranteed" mean in this context? DirectPlay's TCP/IP service provider is implemented using WinSock, which is based on the 4.3BSD UNIX sockets implementation. This implies a very specific definition for what guaranteed means for the TCP/IP service provider, but, suffice to say, with this feature DirectPlay ensures your messages are delivered—and delivered intact. Although you can still download the Internet Explorer for free, very little in this world comes without a cost, and the cost of guaranteed messages is steep. Winsock requires an acknowledgement that the recipient successfully received the message. This typically takes two to three times longer then just sending a message and crossing your fingers—longer if the network is particularly slow. So, ask yourself how lucky you feel. Does your game require low network latencies to be playable? Clearly, you have to make your own decision regarding the merits of guaranteed messaging—the much greater latencies may dictate designing for no guarantees (and occasional lost messages).

Not all DirectPlay service providers support guaranteed messaging. To determine whether or not guaranteed messaging is supported, check the DPCAPS_GUARANTEEDSUPPORTED bit of the dwFlags field in the GetCaps method of the IDirectPlay2 interface (whew!). If guaranteed delivery is supported, the DPCAPS_GUARANTEEDOPTIMIZED flag indicates that the service provider bound to this DirectPlay object supports it directly (otherwise, it is being emulated by DirectPlay). Currently, only the Internet TCP/IP service provider supports guaranteed delivery, and DirectPlay does not emulate it for any of the DirectPlay 3 default providers. The next version of DirectPlay will do this.

Receiving

DirectPlay messages are delivered from other players independently of the Windows message queue, so you won't be calling IDirectPlay2's Receive method in your Windows message pump any time soon. Because using the message pump—and its associated window procedure—is the standard method for processing input to your application, it might make sense to spin off a thread to accept DirectPlay messages from other players and to then post the messages to your window procedure. This is the method used in DPCHAT. However, there is overhead associated with using a separate thread, and there may be some simple applications where a repeating loop to gather input is justified.

Although not the case when you are sending messages, you have no idea when you are going to receive a message from another player. Calling IDirectPlay2's Receive method in a loop is terribly inefficient: because Receive returns immediately when there are no pending messages, this would be a huge waste of CPU time. Instead, DPCHAT uses DirectPlay's synchronization capabilities to signal an event to a sleeping thread when a message arrives for the player id(s) being tracked by your application.

SetupConnection( ) is the initialization routine called from the WinMain( ) function in dpchat.cpp. Here, DPCHAT creates the event and starts the receive thread. Remember that we passed this event handle to DirectPlay when we created the local player identity. Now you can see why the player identification must be associated with a Win32 event handle. Of course, you don't have to use this capability; you can simply pass a NULL event handle to CreatePlayer when you create the user's player identity. The ability to create the event to be signaled allows you to use the same event handle for multiple local player ids.

HRESULT SetupConnection(HINSTANCE hInstance, LPDPLAYINFO lpDPInfo)
{
 LPDPNAME lpName;
 LPSTR  lpszPlayerName;
 HRESULT hr;

 ZeroMemory(lpDPInfo, sizeof(DPLAYINFO));

 // Create event used by DirectPlay to signal a message has arrived.
 lpDPInfo->hPlayerEvent = CreateEvent(NULL,  // no security
          FALSE,  // auto reset
          FALSE,  // initial event reset
          NULL);  // no name
 if (lpDPInfo->hPlayerEvent == NULL)
 {
  hr = DPERR_NOMEMORY;
  goto FAILURE;
 }

 // Create event used to signal that the receive thread should exit.
 ghKillReceiveEvent = CreateEvent(NULL,  // no security
         FALSE,  // auto reset
         FALSE,  // initial event reset
         NULL);  // no name
 if (ghKillReceiveEvent == NULL)
 {
  hr = DPERR_NOMEMORY;
  goto FAILURE;
 }

 // Create thread to receive player messages.
 ghReceiveThread = CreateThread(NULL,   // default security
           0,    // default stack size
           ReceiveThread, // pointer to thread routine
           lpDPInfo,  // argument for thread
           0,    // start it right away
           &gidReceiveThread);
 if (ghReceiveThread == NULL)
 {
  hr = DPERR_NOMEMORY;
  goto FAILURE;
 }

 // Try to connect using the lobby.
 hr = ConnectUsingLobby(lpDPInfo);
 if FAILED(hr)
 {
  // If the error returned is DPERR_NOTLOBBIED, that means the app
  // was not launched by a lobby and we should ask the user for
  // connection settings. If any other error is returned, it means
  // the app was launched by a lobby but there was an error making the
  // connection.

  if (hr != DPERR_NOTLOBBIED)
   ErrorBox("Could not connect using lobby because of error 0x%08X", hr);

  // If there is no lobby connection, ask the user for settings.
  hr = ConnectUsingDialog(hInstance, lpDPInfo);
  if FAILED(hr)
   goto FAILURE;
 }
. . .
FAILURE:
 ShutdownConnection(lpDPInfo);

 return (hr);
}

ReceiveThread( ) in dpchat.cpp blocks on WaitForMultipleObjects( ), waiting for an incoming message. When a message arrives via DirectPlay for this player, the event handle is signaled, allowing WaitForSingleObject( ) to return. Notice that we also block on an event used to signal an intentional demise of the DPCHAT instance. Thus, we use WAIT_OBJECT_0 to enter the while loop and call ReceiveMessage( ); otherwise, the kill event must have been signaled, and we exit the thread.

DWORD WINAPI ReceiveThread(LPVOID lpThreadParameter)
{
   LPDPLAYINFO lpDPInfo = (LPDPLAYINFO) lpThreadParameter;
 HANDLE  eventHandles[2];

 eventHandles[0] = lpDPInfo->hPlayerEvent;
 eventHandles[1] = ghKillReceiveEvent;

 // Loop waiting for player events. If the kill event is signaled,
 // the thread will exit.
 while (WaitForMultipleObjects(2, eventHandles, FALSE, INFINITE) == 
    WAIT_OBJECT_0)
 {
  // Rreceive any messages in the queue.
  ReceiveMessage(lpDPInfo);
 }

 ExitThread(0);

 return (0);
}

ReceiveMessage( ) is a doubly nested Do loop: the Receive( ) processing occurs in an inner loop and handles buffer-size issues; the outer loop deals with the message queue. Note that DPCHAT calls Receive( ) using the default DPRECEIVE_ALL flag that returns the first available message. In this case, both the idTo and idFrom player values are filled in by DirectPlay, whereas (depending on the flag use) the application could fill in one or both of these values. Your application also can specify a flag for mimicking the behavior of PeekMessage, whereby you can receive a message without removing it from the queue.

What necessitates the use of the inner loop in this routine is allocating a right-sized message buffer. DirectPlay always returns the length of the data copied to your buffer in the last parameter. However, when the DPERR_BUFFERTOOSMALL error is returned, this parameter is the required buffer size for the current first message. Note that messages have different priorities; by the time you get the correct buffer size, allocate it, and call Receive( ) again, the message order might have changed—the buffer might still be too small. So the application might have to iterate through this inner loop more times than you expect.

You may think you can be smarter about how to allocate your message buffers, because you know something about the message sizes being passed around. Maybe so, but you can certainly filter on the event handle and on the player identification, including using DPID_SYSMSG for system messages. It may not be worth your time, but the flexibility is there. The loop below continues until the buffer has grown sufficiently large to receive the message.

HRESULT ReceiveMessage(LPDPLAYINFO lpDPInfo)
{
 DPID    idFrom, idTo;
 LPVOID   lpvMsgBuffer;
 DWORD    dwMsgBufferSize;
 HRESULT   hr;

 lpvMsgBuffer = NULL;
 dwMsgBufferSize = 0;

 // Loop to read all messages in queue.
 do
 {
  // Loop until a single message is successfully read.
  do
  {
   // Read messages from any player, including system player.
   idFrom = 0;
   idTo = 0;

   hr = lpDPInfo->lpDirectPlay2A->Receive(&idFrom, &idTo, DPRECEIVE_ALL,
           lpvMsgBuffer,
           dwMsgBufferSize);

   // Not enough room, so resize buffer.
   if (hr == DPERR_BUFFERTOOSMALL)
   {
    if (lpvMsgBuffer)
     GlobalFreePtr(lpvMsgBuffer);
    lpvMsgBuffer = GlobalAllocPtr(GHND, dwMsgBufferSize);
    if (lpvMsgBuffer == NULL)
     hr = DPERR_OUTOFMEMORY;
   }
  } while (hr == DPERR_BUFFERTOOSMALL);

  if (
   // Successfully read a message.
   (SUCCEEDED(hr)) &&
   // and it is big enough
   (dwMsgBufferSize >= sizeof(DPMSG_GENERIC))) 
  {
   // Check for system message.
   if (idFrom == DPID_SYSMSG)
   {
    HandleSystemMessage(lpDPInfo, (LPDPMSG_GENERIC) lpvMsgBuffer,
         dwMsgBufferSize, idFrom, idTo);
   }
   else
   {
    HandleApplicationMessage(lpDPInfo, (LPDPMSG_GENERIC) lpvMsgBuffer,
           dwMsgBufferSize, idFrom, idTo);
   }
  }
 } while (SUCCEEDED(hr));

 // Free any memory we created.
 if (lpvMsgBuffer)
  GlobalFreePtr(lpvMsgBuffer);

 return (DP_OK);
}

Handling DirectPlay Messages. Having successfully received a message, the actual message processing is being shuffled off to separate message handlers. Presumably, you know what you want to do with your own application messages. DPCHAT simply posts the message to its main window procedure using the same mechanism it uses for locally generated strings. This is shown in HandleApplicationMessage( ).

void HandleApplicationMessage(LPDPLAYINFO lpDPInfo, 
         LPDPMSG_GENERIC lpMsg, 
         DWORD dwMsgSize,
         DPID idFrom, DPID idTo)
{
 LPSTR lpszStr = NULL;
 HRESULT hr;

 switch (lpMsg->dwType)
 {
 case APPMSG_CHATSTRING:
        {
         LPMSG_CHATSTRING   lp = (LPMSG_CHATSTRING) lpMsg;

   // Create string to display.
   hr = NewChatString(lpDPInfo->lpDirectPlay2A, idFrom, 
                 lp->szMsg, &lpszStr);
   if FAILED(hr)
    break;
        }
  break;
 }

 // Post string to chat window.
 if (lpszStr)
 {
  // Make sure window is still valid.
  if (ghChatWnd)
   PostMessage(ghChatWnd, WM_USER_ADDSTRING, (WPARAM) 0, 
                    (LPARAM) lpszStr);
  else
   GlobalFreePtr(lpszStr);
 }
}

DirectPlay generates system messages that notify players of session changes. As you scan through these messages, notice that some are generated as a result of session parameters established in the DPSESSIONDESC2 structure, whereas others happen without regard to the session parameters. How you handle system messages depends on the nature of your application. Before examining the way DPCHAT handles system messages, it might be useful to look at the details of the process. It also might be useful for you to put a breakpoint at each message in order to see when (and why) the messages are generated.

All system messages come from player DPID_SYSMSG, and each one has an associated message structure containing the information relevant to that message identification. The lpData parameter of Receive( ) is a pointer to the message structure for each message. Although each message uses a different structure, all begin with a double word containing the message identification. You can cast this pointer into the generic message structure and switch on it, as shown in DPCHAT below. In handling each message, you can then recast the lpData pointer into the proper message structure.

void HandleSystemMessage(LPDPLAYINFO lpDPInfo, LPDPMSG_GENERIC lpMsg, 
         DWORD dwMsgSize,
         DPID idFrom, DPID idTo)
{
 LPSTR  lpszStr = NULL;

    // The body of each case is there so you can set a breakpoint and examine
    // the contents of the message received.
 switch (lpMsg->dwType)
 {
 case DPSYS_CREATEPLAYERORGROUP:

This message is sent when you call IDirectPlay2::CreateGroup or ::CreatePlayer. The message data includes a flag indicating player or group, its ID, a pointer to remote data being established at create time (you can't create local data at create time), the remote data size, and the text name of the player or group.

        {
            LPDPMSG_CREATEPLAYERORGROUP 
     lp = (LPDPMSG_CREATEPLAYERORGROUP) lpMsg;
    LPSTR lpszPlayerName;
    LPSTR szDisplayFormat = "\"%s\" has joined\r\n";
            
   // Get pointer to player name.
   if (lp->dpnName.lpszShortNameA)
    lpszPlayerName = lp->dpnName.lpszShortNameA;
   else
    lpszPlayerName = "unknown";

   // Allocate space for string.
   lpszStr = (LPSTR) GlobalAllocPtr(GHND, strlen(szDisplayFormat) +
                                          strlen(lpszPlayerName) + 1);
   if (lpszStr == NULL)
    break;

   // Build string.
   wsprintf(lpszStr, szDisplayFormat, lpszPlayerName);
        }
  break;

 case DPSYS_DESTROYPLAYERORGROUP:

This message is sent when you call IDirectPlay2::DestroyGroup or ::DestroyPlayer. The message data includes a flag indicating player or group, its ID, pointers to any remote and local data, and the data sizes.

        {
            LPDPMSG_DESTROYPLAYERORGROUP lp =
                 (LPDPMSG_DESTROYPLAYERORGROUP)lpMsg;
    LPSTR lpszPlayerName;
    LPSTR szDisplayFormat = "\"%s\" has left\r\n";
            
   // Get pointer to player name stored in remote data.
   if ((lp->dwRemoteDataSize) && (lp->lpRemoteData))
    lpszPlayerName = (LPSTR) lp->lpRemoteData;
   else
    lpszPlayerName = "unknown";

   // Allocate space for string.
   lpszStr = (LPSTR) GlobalAllocPtr(GHND, strlen(szDisplayFormat) +
           strlen(lpszPlayerName) + 1);
   if (lpszStr == NULL)
    break;

   // Build string.
   wsprintf(lpszStr, szDisplayFormat, lpszPlayerName);
        }
  break;

 case DPSYS_ADDPLAYERTOGROUP:

This message is sent when you call IDirectPlay2::AddPlayerToGroup. The message data includes the player and group IDs.

        {
            LPDPMSG_ADDPLAYERTOGROUP lp = (LPDPMSG_ADDPLAYERTOGROUP)lpMsg;
            if ( lp );
        }
  break;

 case DPSYS_DELETEPLAYERFROMGROUP:

This message is sent when you call IDirectPlay2::DeletePlayerFromGroup. The message data includes the player and group IDs.

        {
            LPDPMSG_DELETEPLAYERFROMGROUP lp =
               (LPDPMSG_DELETEPLAYERFROMGROUP)lpMsg;
            if ( lp );
        }
  break;

 case DPSYS_SESSIONLOST:

This message is sent only if the DPSESSION_KEEPALIVE session option is specified when the session is created. The DPSESSION_KEEPALIVE flag tells DirectPlay to take note if a local player's connection to the session has been lost. The exact method of determining this depends on the service provider. A modem service provider, for example, can easily detect if the carrier is lost, whereas the Internet service provider has to use a more complicated scheme of pinging each player. Either way, this message is only sent to the local player whose connection has been hosed. At a minimum, your application must handle gracefully the case of the session being lost. You might be able to do more than that—attempting to rejoin the session, for example—depending on the nature of your application.

The message data does not contain any fields beyond the dwType value identifying the DPSYS_SESSIONLOST system message identification.

        {
            LPDPMSG_SESSIONLOST lp = (LPDPMSG_SESSIONLOST)lpMsg;
            if ( lp );
        }
  break;

 case DPSYS_HOST:

This message is sent only if the DPSESSION_MIGRATEHOST session option is specified when the session is created. The DPSESSION_MIGRATEHOST flag instructs DirectPlay to migrate the session host to another remote player when the host either loses his or her connection or is removed from the session. This way, new players and applications can continue to join the session after the host quits. The message is sent to the player assuming host responsibilities.

The message data does not contain any fields beyond the dwType value identifying the DPSYS_HOST system message ID.

        {
            LPDPMSG_HOST lp = (LPDPMSG_HOST)lpMsg;
    LPSTR  szDisplayFormat = "You have become the host\r\n";

   // Allocate space for string.
   lpszStr = (LPSTR) GlobalAllocPtr(GHND, strlen(szDisplayFormat) + 1);
   if (lpszStr == NULL)
    break;

   // Build string.
   strcpy(lpszStr, szDisplayFormat);

   // We are now the host.
   lpDPInfo->bIsHost = TRUE;
        }
  break;

 case DPSYS_SETPLAYERORGROUPDATA:

This message is sent when remote-player or group data has changed. The message data identifies the group or player ID whose data has changed, a pointer to the remote data, and its size.

       {
            LPDPMSG_SETPLAYERORGROUPDATA lp = 
               (LPDPMSG_SETPLAYERORGROUPDATA)lpMsg;
            if ( lp );
        }
  break;

 case DPSYS_SETPLAYERORGROUPNAME:

This message is sent when a remote-player or group name has changed via IDirectPlay2::SetPlayerName or ::SetGroupName. The message data identifies the group or player identification whose name has changed and the new name.

        {
            LPDPMSG_SETPLAYERORGROUPNAME lp = 
     (LPDPMSG_SETPLAYERORGROUPNAME)lpMsg;
            if ( lp );
        }
  break;
 }

 // Post string to chat window.
 if (lpszStr)
 {
  // Make sure window is still valid.
  if (ghChatWnd)
   PostMessage(ghChatWnd, WM_USER_ADDSTRING, (WPARAM) 0, 
          (LPARAM) lpszStr);
  else
   GlobalFreePtr(lpszStr);
 }
}

Remote Data

The ability to replicate remote data across the session is a feature new to DirectPlay 3. Remote data can be retrieved by other instances of your application by associating it with the player or group ID value established by IDirectPlay2::CreatePlayer or ::CreateGroup. The remote aspect of this functionality can also be disabled; you can, in other words, use this feature to locally stash player- or group-specific data. Using the remote vs. local capability of this feature could be likened to the difference between Get/SetWindowLong( ) and Get/SetClassLong( ), where the application data is only established with a given instance of a window or can be associated with all instances of that window's class.

Remote data is best used for global state data that is available to all players in the session and can be retrieved at any point. If the data is static, this automated form of data replication can really simplify your work; otherwise, for dynamically changing data, you need to be careful, because changing even a single byte causes the entire block to be resent. A DPSYS_SETPLAYERORGROUPDATA system message is generated to all players in the session when the data has changed.

DPCHAT establishes remote data in the SetupConnection( ) function. The player's name is replicated across the session, so the DPSYS_DESTROYPLAYERORGROUP message handling in HandleSystemMessage( ) can retrieve the name of the person leaving the chat session for posting to each local chat window.

Where To Go from Here

Now that you've mastered DPCHAT and have a good idea of how to use DirectPlay, you might want to check out another DirectPlay application. Try DUEL, a sample game available in the DirectX SDK. What makes this application especially interesting is its heritage as a DirectX 2 SDK sample application for DirectDraw. It pitted a single player against the computer in a typical twitch game. The DUEL sample was updated for the DirectX3 SDK to support multiplayer gaming via DirectPlay3. So, if you are curious, compare the sources from the DirectX 2 and DirectX 3 SDKs with a view toward understanding what it takes to port a DirectX, single-player application to a multiplayer game via DirectPlay3.

If you are thinking of adding a multiplayer option to a game that is not multiplayer, another upcoming article will be helpful—"Developing Real-Time Multiplayer Games for the Internet." We don't promise to solve all your problems, but we intend to help you squeeze the most performance out of DirectPlay and the Internet.

Stay tuned.