Anet Transport DLL API


note: parts of this document are still out of date

Introduction

Anet can support different communications media and protocols because the subroutines that actually send and receive raw packets are kept in drivers (also known as transport DLL's). Each driver has a name (e.g. "Internet", "Modem", "IPX"). The function dpEnumTransports gives a list of the available transports and their names, and is used by the shell to let the user choose a connection method. Anet ships with four transports: IPX, Internet, serial, and modem.

New network protocols or connection methods can be supported by writing new driver DLL's. A separate DLL must be written for the desired communication medium and protocol for each platform you wish to support - DOS, Win32, Mac, or Unix. For instance, under DOS, we wrote a new transport DLL to support Dwango, and under Windows, we wrote a loopback driver which lets you develop games without having to connect to a second computer.

Software Architecture

The Stub and Netshell

The first thing that starts when the user clicks on a game icon on the Windows or Mac desktop is the stub, a program whose purpose is to load the shell (which sets up the game session) and then the selected game. The shell prompts the user to select one of the available transport layers, then loads the appropriate comm DLL(s) and prompts the user to start or join a game.

Once a game has been selected, the shell shuts down the comm DLLs and exits, and the game program itself starts. (This is handled by dpLaunchApp(). In DOS, dpLaunchApp() tells the stub to launch the game program, then terminates itself; in Windows and Mac, dpLaunchApp() directly launches the game program, then teriminates itself). The game program loads the same comm DLLs as the shell, plays the game, shuts down the comm DLLs, and exits. The stub starts the shell again and the user is prompted to join another game.

The comm DLLs cannot persist across invocations of the shell and the game program because the shell exits before the game program starts up, and vice versa. The dpio layer has a function dpio_freeze() which writes out enough information to set up the connections again; the function dpio_create() can read that file and reinitialize the comm DLL's.

A trick will be used on Win95 to keep the handle to the modem's comm port open - it will be marked as inheritable by the netshell, and the value of the handle will be written to disk by dpio_freeze(). A similar trick will also be needed on the Mac for the modem driver.

Games can also run standalone without the stub and netshell, but they then have to implement those functions themselves.

Messages

All communication is performed in terms of messages. Message types and contents are defined by upper layers (dpio, dp, and the game itself).

The actual way in which a message is transmitted is defined by the transport layer. For instance, the point-to-point null-modem DLL supplied with Anet performs its own data compression and error correction.

The whole point of the transport DLL is the pair of functions CommTxPkt and CommRxPkt.
The upper layer sends a message by calling the transport layer's CommTxPkt routine. Each arriving message is queued up for retrieval by the upper layer. The upper layer retrieves a message (and the player handle of the node which sent it) by calling the DLL's CommRxPkt routine.

The transport layer cannot handle an unlimited number of simultaneous messages. Therefore, it is possible for messages to be dropped silently, if memory begins to run low.

Player Handles

Messages are directed to and from nodes via "player handles." A player handle can refer to a single node, or to all nodes within earshot (where the exact meaning of "within earshot" is defined by the particular comm medium and protocol).

Note: these really should be called node handles.

Communication between the dpio layer and the transport layer is done in terms of player handles. A player handle is an integer. Player handles are assigned dynamically by the transport layer and are valid only until the transport layer is unloaded (typically when the upper layer exits).

Two nodes may refer to a third node via player handles which are not identical. That is, player handles are assigned independently by each node; often, the first node you connect to gets handle 1, the second node gets handle 2, etc. But some drivers assign handles differently, e.g. the UUDP driver under Linux returns the IP address as the handle.

The transport layer learns about nodes in only one way: when the upper layer calls the commSayHi function. If a packet has been received from a node for which a player handle has not yet been allocated with commSayHi, the source node will be identified as PLAYER_UNKNOWN. The upper layer should get the address of the current node using commPlayerInfo, then embed that address in a packet, and broadcast it (or send it to a well-known game server). When packets containing addresses are received, the upper layer can call commSayHi if it wishes to send packets to the nodes which advertized their address.

Predefined Handles

Not all player handles refer to individual players. There are also "non-player handles." There are also certain predefined handles. A node handle is a single handle referring to a single node (computer). This is the normal case, and the only one currently implemented.

The predefined handles include:

Network Addresses

Transport DLLs normally have access to a network address (e.g. the computer's IPX address). The API requires and relies on network addresses. So, what should a transport DLL for a non-addressed communications medium, such as null modem, modem, or loopback driver do? It turns to the sessionID field of the commInitReq_t structure passed to commInit, which is a 32 bit random number which should be assigned by the shell on initial startup, and passed to each succeeding program that needs to call commInit in the current game session.

Connections

A node cannot send to another node unless it has opened a connection handle to the other node. Each DLL has its own concept of what it means to be "connected" to a node.

In some cases, such as a local IP network, two nodes can communicate without any setup. In other cases, such as a modem-to-modem link, a node cannot communicate with another node until it has dialed the phone. In still other cases, such as a dial-up hub service, it is possible for a node with a modem to communicate with two or more other nodes without hanging up and redialing.

Establishing a Connection

When a node wishes to communicate with another node, it can simply call the appropriate DLL's CommTxPkt routine, if the DLL has already assigned a player handle to the destination node.

If the DLL has not assigned a player handle to the destination node, the upper layer cannot yet use CommTxPkt, since CommTxPkt expects a player handle. There are at least four ways around this situation:

Breaking a Connection

The DLL's commSayBye routine allows the upper layer to terminate any connection explicitly. This is rarely needed, except at the end of the game or when it is fairly certain that the user will not need to communicate with a particular node any more. Upon disconnection, the node's player handle becomes available for reuse.

Disconnecting from predefined handles is not allowed.

Connection Information

Data passed to commSayHi must be in the format expected by the DLL - often an unprintable data stream.

However, users may need to view or edit connection information, which means it must be presented in a readable format. Certain communications media cannot even be bootstrapped until connection information is provided by the user. For instance, without a phone number to dial, a modem transport layer cannot originate even a single connection.

To format a connection information block into a readable string, the upper layer can call the DLL's CommPrintAddr routine. To convert readable data to a connection information block, the upper layer can call the DLL's CommScanAddr routine.

Setting Up a Game

It is important to remember that many of the concepts traditionally associated with a game are the responsibility of the shell and/or the game, not the transport layer. A transport layer DLL exists principally for the purpose of sending and receiving messages.

In that sense, it is like a file I/O package, which reads and writes data and refers to its targets (files) via handles. Neither the file I/O package nor the transport layer is particularly concerned with the contents of the data streams it manipulates.

The following is a brief synopsis of the steps needed to establish a game using Anet's transport DLLs.

Note: this is normally left to the DP package. The following section is mostly of interest for people trying to understand how DP and dpio use the comm dll's.

Choosing the Media

The user starts up the pre-executable. The pre-execuble searches in the local directory for transport layer DLL files and reports each one to the user. In this way, the user can select one (or more) transport layers.

The pre-executable launches the shell, passing it a command-line switch indicating the selected comm medium and protocol. The shell reads this switch, loads the appropriate comm DLL using dpLoadDLL(), and calls the DLL's CommInit routine, to perform DLL-specific initializations.

If the CommInit routine returns a failure status, the DLL is unusable and the shell complains and exits.

Enumerating the Sessions

The first order of business for the shell is to find out which games are being set up "out there" in the big wide world. This is done by sending a "who's there" message, which is passed to the DLL's CommTxPkt routine along with the PLAYER_BROADCAST player handle.

Since the concept of broadcasting is protocol-dependent, the transport layer sends out the "who's there" message in whatever manner, and to whatever destination(s), it chooses. In some cases, such as modem, the message may go to at most a single node. In other cases, such as IP, the message may be broadcast over a local network.

When a "who's there" message arrives at a node, the transport layer passes it up to the shell. Any node's shell can respond to a "who's there", but typically only the hosts of games will do so. If a non-host responds, this is usually because it suspects that the host of its game is not directly reachable by the sender of the "who's there."

It is not necessary for every node that hears the "who's there" to respond, but those that do not respond run the risk of never being contacted by the sender again (not always a disadvantage).

To respond to a "who's there" message, a node's shell sends an "I'm here" message. The "I'm here" message may include such information as whether the node is hosting a game (and the name of the game), other media supported by the node, and the node's physical location.

Joining a Game

As each "I'm here" message comes in to the original sender, the transport layer assigns a new player handle to the sender (if it has not seen a message from that node before) and queues the message for delivery to the shell. In this way, the shell learns which nodes are "out there."

Over time, the shell acquires a list of active game hosts, which it can present to the user. If the user selects a host to query, the shell can transmit messages to that host to get further information (such as the number of available spaces on the game, the mission, and the names of the other players). It may also attempt to contact each of the other players in that game, in an effort to determine whether throughput and connectivity will hinder game play.

Finally, the user selects a game to play. The shell sends a message to the host to indicate that it wants to join the game, and the host responds (presumably in the affirmative). Before the user's shell exits, it saves the connection information for the host (and for as many other nodes as it needs to, including non-players) in a file and unloads the transport layer DLL. When the game is launched, it reloads the transport layer DLL, reads the connection information back in from the file, establishes connections to as many of the nodes as it needs to, and begins the game.

Hosting a Game

To host a game, the shell can broadcast an "I am hosting" messaage or not, as it chooses. Nodes coming on-line will eventually broadcast "who's there" messages, which are received and given player handles by the host's transport layer. The host responds with an "I'm here" message indicating that he is a host, the new node sends a "may I join" message, and the game is gradually built up.

Quitting

At shutdown time, the upper layer calls the DLL's CommTerm routine, to perform DLL-specific terminations. It can then unload the DLL.

Miscellany

MS-DOS DLL's and the C Runtime Library

Under MS-DOS, transport layer DLLs can make calls to the standard C runtime library. The library is linked in with the DLL, making self-contained routines such as strcpy() available.

However, many routines require access to system-wide resources. For example, malloc() expects that DOS has given it a pointer to available memory. Since DOS knows nothing about the DLL, this will fail.

To get around many of these problems, the DLL can include a jump table which vectors into the various entry points in the executable's version of the C runtime library. That is, to call malloc(), the DLL would actually call a thunk of code which jumps through the jump table into the malloc() in the C runtime library linked in by the executable that loaded the DLL.

There are a few exceptions to this, however. The most notable is file I/O, which does not work because it depends on startup code (which is never run in the DLL) and on __iob[], a constant.

Other API Calls

The API includes a CommNoOp routine, whose purpose is to verify that the DLL is accessible. This is mostly useful during debugging.

The CommDriverInfo routine returns dynamic information about the DLL.

The CommPlayerInfo routine returns dynamic information about the given player handle.

There is a way to report scores to the underlying network. See the Dwango driver and dpReportScore for an example.

User interface inside Transport DLL's

Under Windows and DOS, the transport DLL's do not provide any user interface. Well, that's not quite true: hitting the ESCAPE key during modem dialing will abort the dialing process, but no feedback is given to the user about this.

On the Mac, Howard Shere implemented a scheme whereby the transport DLL was passed a screen rectangle which it could use, and used this for the user interface associated with CommToolbox calls. This scheme currently lies dormant in the Mac code for the modem and serial drivers; when we try to get those drivers working again, we might need to add a screen rectangle and/or window handle field into the commInitReq_t passed to commInit(). This needs to be better defined, probably with help from the fellow doing the Netshell.

API

The complete API for the Anet transport layer DLLs is defined by commapi.h.